├── doc ├── hooks.md ├── polymorphism.md ├── enums.md ├── types.md ├── codec.md ├── formats.md ├── generics.md └── models.md ├── example └── README.md ├── codable_builder ├── CHANGELOG.md ├── analysis_options.yaml ├── lib │ ├── codable_builder.dart │ └── src │ │ ├── models │ │ ├── urls.dart │ │ ├── types.dart │ │ └── nodes.dart │ │ ├── codable_builder.dart │ │ ├── builders │ │ ├── decode_mixin.dart │ │ ├── encode_mixin.dart │ │ ├── polymorphic_mixin.dart │ │ ├── codable_builder.dart │ │ └── enum_builder_mixin.dart │ │ └── analyzer │ │ └── nodes.dart ├── test │ ├── with_analyzer │ │ ├── sources │ │ │ └── basic_class.dart │ │ └── codable_class_test.dart │ ├── utils.dart │ ├── codable_member_test.dart │ ├── generics_test.dart │ └── polymorphics_test.dart ├── .gitignore ├── pubspec.yaml └── README.md ├── CHANGELOG.md ├── analysis_options.yaml ├── lib ├── msgpack.dart ├── csv.dart ├── json.dart ├── progressive_json.dart ├── standard.dart ├── common.dart ├── core.dart ├── extended.dart └── src │ ├── extended │ ├── custom.dart │ ├── lazy.dart │ ├── generics.dart │ ├── hooks.dart │ ├── reference.dart │ └── inheritance.dart │ ├── codec │ ├── msgpack.dart │ ├── progressive_json.dart │ ├── csv.dart │ ├── standard.dart │ ├── json.dart │ └── codec.dart │ ├── common │ ├── uri.dart │ ├── datetime.dart │ ├── map.dart │ ├── object.dart │ └── iterable.dart │ ├── helpers │ └── binary_tokens.dart │ └── core │ └── errors.dart ├── .gitignore ├── dart_test.yaml ├── pubspec.yaml ├── codegen.md ├── test ├── hooks │ ├── basic │ │ └── model │ │ │ └── game.dart │ ├── proxy_hook.dart │ ├── delegate.dart │ └── complex │ │ └── model │ │ └── box.dart ├── polymorphism │ ├── multi_interface │ │ ├── multi_interface_test.dart │ │ └── model │ │ │ └── material.dart │ └── basic │ │ ├── poly_test.dart │ │ └── model │ │ └── pet.dart ├── enum │ ├── enum_test.dart │ └── model │ │ └── color.dart ├── generics │ └── basic │ │ ├── model │ │ └── box.dart │ │ └── basic_test.dart ├── mappable │ ├── simple.dart │ └── mapper.dart ├── progressive_json │ └── model │ │ ├── circle.dart │ │ ├── async_value.dart │ │ └── person.dart ├── async │ └── model │ │ └── result.dart ├── basic │ ├── test_data.dart │ ├── basic_test.dart │ └── model │ │ └── person.dart ├── csv │ ├── test_data.dart │ ├── csv_test.dart │ └── model │ │ └── measures.dart ├── benchmark │ ├── bench.dart │ └── performance_test.dart ├── error_handling │ └── error_handling_test.dart ├── collections │ └── collections_test.dart └── utils.dart ├── LICENSE └── ABOUT.md /doc/hooks.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/polymorphism.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | See examples in RFC and test/ folder 2 | -------------------------------------------------------------------------------- /codable_builder/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | - Initial version. 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0-beta.0 2 | 3 | - Initial prototype implementation. 4 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | analyzer: 4 | enable-experiment: 5 | - macros 6 | -------------------------------------------------------------------------------- /codable_builder/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | linter: 4 | rules: 5 | prefer_relative_imports: true -------------------------------------------------------------------------------- /codable_builder/lib/codable_builder.dart: -------------------------------------------------------------------------------- 1 | export 'src/models/nodes.dart'; 2 | export 'src/models/types.dart'; 3 | export 'src/codable_builder.dart'; 4 | -------------------------------------------------------------------------------- /lib/msgpack.dart: -------------------------------------------------------------------------------- 1 | // Decoder and Encoder implementations for MessagePack format. 2 | library; 3 | 4 | export 'src/codec/msgpack.dart'; 5 | export 'src/formats/msgpack.dart'; 6 | -------------------------------------------------------------------------------- /lib/csv.dart: -------------------------------------------------------------------------------- 1 | /// Decoder and Encoder implementations for CSV (Comma-Separated Values) format. 2 | library; 3 | 4 | export 'src/codec/csv.dart'; 5 | export 'src/formats/csv.dart'; 6 | -------------------------------------------------------------------------------- /lib/json.dart: -------------------------------------------------------------------------------- 1 | /// Decoder and Encoder implementations for JSON (JavaScript Object Notation) format. 2 | library; 3 | 4 | export 'src/codec/json.dart'; 5 | export 'src/formats/json.dart'; 6 | -------------------------------------------------------------------------------- /codable_builder/test/with_analyzer/sources/basic_class.dart: -------------------------------------------------------------------------------- 1 | class TestClass { 2 | final String name; 3 | final int age; 4 | final bool isActive; 5 | TestClass(this.name, this.age, this.isActive); 6 | } -------------------------------------------------------------------------------- /lib/progressive_json.dart: -------------------------------------------------------------------------------- 1 | /// Decoder and Encoder implementations for Progressive JSON format. 2 | library; 3 | 4 | export 'src/codec/progressive_json.dart'; 5 | export 'src/formats/progressive_json.dart'; 6 | -------------------------------------------------------------------------------- /codable_builder/lib/src/models/urls.dart: -------------------------------------------------------------------------------- 1 | 2 | const String codableCoreUrl = 'package:codable_dart/core.dart'; 3 | const String codableExtendedUrl = 'package:codable_dart/extended.dart'; 4 | const String dartCoreUrl = 'dart:core'; 5 | -------------------------------------------------------------------------------- /lib/standard.dart: -------------------------------------------------------------------------------- 1 | /// Decoder and Encoder implementations for converting serialized data to Dart objects like Maps and Lists, and vice versa. 2 | library; 3 | 4 | export 'src/codec/standard.dart'; 5 | export 'src/formats/standard.dart'; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/libraries/private-files 2 | # Created by `dart pub` 3 | .dart_tool/ 4 | 5 | # Avoid committing pubspec.lock for library packages; see 6 | # https://dart.dev/guides/libraries/private-files#pubspeclock. 7 | pubspec.lock 8 | -------------------------------------------------------------------------------- /codable_builder/.gitignore: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/libraries/private-files 2 | # Created by `dart pub` 3 | .dart_tool/ 4 | 5 | # Avoid committing pubspec.lock for library packages; see 6 | # https://dart.dev/guides/libraries/private-files#pubspeclock. 7 | pubspec.lock 8 | -------------------------------------------------------------------------------- /dart_test.yaml: -------------------------------------------------------------------------------- 1 | # Run benchmarks using 'dart test -P benchmark' 2 | 3 | tags: 4 | benchmark: 5 | skip: true 6 | presets: { benchmark: { skip: false } } 7 | 8 | presets: 9 | benchmark: 10 | include_tags: benchmark 11 | concurrency: 1 12 | compilers: 13 | - exe 14 | -------------------------------------------------------------------------------- /lib/common.dart: -------------------------------------------------------------------------------- 1 | /// Codable implementations for common data types, such as DateTime, Uri, List, Iterable, Map and Object. 2 | library; 3 | 4 | export 'src/common/datetime.dart'; 5 | export 'src/common/uri.dart'; 6 | export 'src/common/iterable.dart'; 7 | export 'src/common/map.dart'; 8 | export 'src/common/object.dart'; -------------------------------------------------------------------------------- /lib/core.dart: -------------------------------------------------------------------------------- 1 | /// API for encoding and decoding objects to various data formats. 2 | /// 3 | /// This library contains the core API interfaces and classes. 4 | library; 5 | 6 | export 'src/core/interface.dart'; 7 | export 'src/core/decoder.dart'; 8 | export 'src/core/encoder.dart'; 9 | export 'src/core/errors.dart'; 10 | -------------------------------------------------------------------------------- /codable_builder/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: codable_builder 2 | description: Code builders for codable. 3 | version: 1.0.0 4 | 5 | environment: 6 | sdk: ^3.6.0 7 | 8 | dependencies: 9 | code_builder: ^4.10.1 10 | analyzer: ^7.5.9 11 | 12 | dev_dependencies: 13 | dart_style: ^3.1.1 14 | lints: ^5.0.0 15 | test: ^1.24.0 16 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: codable_dart 2 | description: A codable protocol for Dart. 3 | version: 0.1.0-beta.0 4 | repository: https://github.com/schultek/codable 5 | 6 | environment: 7 | sdk: ^3.5.0 8 | 9 | topics: 10 | - codable 11 | - serialization 12 | - json 13 | 14 | dependencies: 15 | async: ^2.13.0 16 | meta: ^1.16.0 17 | 18 | dev_dependencies: 19 | collection: ^1.19.1 20 | lints: ^4.0.0 21 | test: ^1.24.0 22 | type_plus: ^2.1.1 23 | -------------------------------------------------------------------------------- /lib/extended.dart: -------------------------------------------------------------------------------- 1 | /// Extended functionality for Codables, including generics, inheritance, async support, chunked decodeing and hooks. 2 | library; 3 | 4 | export 'src/extended/compat.dart'; 5 | export 'src/extended/generics.dart'; 6 | export 'src/extended/custom.dart'; 7 | export 'src/extended/inheritance.dart'; 8 | export 'src/extended/async.dart'; 9 | export 'src/extended/lazy.dart'; 10 | export 'src/extended/reference.dart'; 11 | export 'src/extended/hooks.dart'; 12 | -------------------------------------------------------------------------------- /lib/src/extended/custom.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | 3 | abstract base class CustomTypeDelegate implements Codable { 4 | const CustomTypeDelegate(); 5 | 6 | DecodingType? _checkValue(dynamic value) => value is T ? DecodingType.custom() : null; 7 | } 8 | 9 | extension CustomTypeExtension on Iterable { 10 | DecodingType? whatsNext(dynamic value) => map((e) => e._checkValue(value)).whereType().firstOrNull; 11 | } 12 | -------------------------------------------------------------------------------- /codable_builder/lib/src/codable_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:code_builder/code_builder.dart'; 2 | 3 | import 'builders/codable_builder.dart'; 4 | import 'models/nodes.dart'; 5 | 6 | abstract class CodableBuilder { 7 | const factory CodableBuilder() = CodableBuilderImpl; 8 | 9 | Spec buildStaticCodableMember(CodableClassNode node); 10 | 11 | Method buildSelfEncodeMethod(CodableClassNode node); 12 | 13 | Extension? buildGenericUseExtension(CodableClassNode node); 14 | 15 | Class buildCodableClass(CodableClassNode node); 16 | 17 | Class buildCodableEnumClass(CodableEnumNode node); 18 | } 19 | -------------------------------------------------------------------------------- /codegen.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Class: 4 | - Mixin or Extension 5 | - isAbstract 6 | - Config 7 | - caseStyle 8 | - ignoreNull 9 | - generateMethods 10 | - classHook 11 | - shallowEncoding 12 | - includeTypeId 13 | 14 | - fields 15 | - param? 16 | - config 17 | - key 18 | - default value 19 | - hook 20 | - generateMethods 21 | - mapper/codable 22 | - subclassing 23 | - isSubclass 24 | - discriminatorKey 25 | - discriminatorValue 26 | 27 | 28 | Enum: 29 | - Values 30 | - Config 31 | - mode 32 | - defaultValue 33 | - hook 34 | 35 | Records: 36 | - fields -------------------------------------------------------------------------------- /lib/src/codec/msgpack.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:codable_dart/msgpack.dart'; 4 | import 'package:codable_dart/src/core/interface.dart'; 5 | 6 | import '../../common.dart'; 7 | import 'codec.dart'; 8 | 9 | const Codec> msgPack = CodableCodec(_MsgPackDelegate(), ObjectCodable()); 10 | 11 | class _MsgPackDelegate extends CodableCodecDelegate> { 12 | const _MsgPackDelegate(); 13 | 14 | @override 15 | T decode(List input, Decodable using) => MsgPackDecoder.decode(input, using); 16 | 17 | @override 18 | List encode(T input, Encodable using) => MsgPackEncoder.encode(input, using: using); 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/common/uri.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | 3 | /// A [Uri] codable that can encode and decode an uri as a string. 4 | class UriCodable implements Codable { 5 | const UriCodable(); 6 | 7 | @override 8 | Uri decode(Decoder decoder) { 9 | return switch (decoder.whatsNext()) { 10 | DecodingType.string || DecodingType.unknown => Uri.parse(decoder.decodeString()), 11 | DecodingType() => decoder.decodeObject(), 12 | _ => decoder.expect('string or custom uri'), 13 | }; 14 | } 15 | 16 | @override 17 | void encode(Uri value, Encoder encoder) { 18 | if (encoder.canEncodeCustom()) { 19 | encoder.encodeObject(value); 20 | } else { 21 | encoder.encodeString(value.toString()); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/hooks/basic/model/game.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | import 'package:codable_dart/extended.dart'; 3 | 4 | import '../../proxy_hook.dart'; 5 | 6 | class Game implements SelfEncodable { 7 | String name; 8 | 9 | Game(this.name); 10 | 11 | @override 12 | void encode(Encoder encoder) { 13 | encoder.encodeKeyed() 14 | ..encodeString('name', name) 15 | ..end(); 16 | } 17 | } 18 | 19 | class GameHook with ProxyHook { 20 | const GameHook(); 21 | 22 | @override 23 | String visitString(String value) { 24 | return value.toUpperCase(); 25 | } 26 | } 27 | 28 | class GameCodable extends CodableWithHooks { 29 | GameCodable() : super(const _GameCodable(), const GameHook()); 30 | } 31 | 32 | class _GameCodable extends SelfCodable { 33 | const _GameCodable(); 34 | 35 | @override 36 | Game decode(Decoder decoder) { 37 | final keyed = decoder.decodeMapped(); 38 | return Game( 39 | keyed.decodeString('name'), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /codable_builder/test/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:analyzer/dart/analysis/results.dart'; 4 | import 'package:analyzer/dart/analysis/utilities.dart'; 5 | import 'package:analyzer/dart/element/element2.dart'; 6 | import 'package:codable_builder/codable_builder.dart'; 7 | import 'package:code_builder/code_builder.dart'; 8 | import 'package:dart_style/dart_style.dart'; 9 | 10 | const builder = CodableBuilder(); 11 | 12 | final _emitter = DartEmitter(useNullSafetySyntax: true); 13 | final _formatter = DartFormatter(languageVersion: DartFormatter.latestLanguageVersion); 14 | 15 | String emit(Spec code) { 16 | final output = code.accept(_emitter).toString(); 17 | return _formatter.format(output); 18 | } 19 | 20 | 21 | Future getElementFrom(String path) async { 22 | final absolutePath = File('test/with_analyzer/sources/$path').absolute.path; 23 | 24 | final result = await resolveFile2(path: absolutePath); 25 | if (result is! ResolvedUnitResult) { 26 | throw Exception('Failed to resolve the file: $result'); 27 | } 28 | 29 | return result.libraryElement2.classes.first; 30 | } -------------------------------------------------------------------------------- /lib/src/codec/progressive_json.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:codable_dart/core.dart'; 5 | import 'package:codable_dart/src/codec/codec.dart'; 6 | 7 | import '../../common.dart'; 8 | import '../formats/progressive_json.dart'; 9 | 10 | const Codec> progressiveJson = CodableCodec(_ProgressiveJsonDelegate(), ObjectCodable()); 11 | 12 | class _ProgressiveJsonDelegate extends CodableCodecDelegate> { 13 | const _ProgressiveJsonDelegate(); 14 | 15 | @override 16 | T decode(List input, Decodable using) => ProgressiveJsonDecoder.decodeSync(input, using); 17 | 18 | @override 19 | List encode(T input, Encodable using) => ProgressiveJsonEncoder.encodeSync(input, using: using); 20 | 21 | @override 22 | Sink> startChunkedConversion(Sink sink, Decodable decodable) { 23 | final controller = StreamController>(); 24 | 25 | final out = ProgressiveJsonDecoder.decode(controller.stream, decodable); 26 | out.listen(sink.add, onDone: sink.close); 27 | 28 | return controller; 29 | } 30 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Kilian Schulte 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. -------------------------------------------------------------------------------- /lib/src/codec/csv.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:codable_dart/core.dart'; 4 | import 'package:codable_dart/csv.dart'; 5 | import 'package:codable_dart/src/codec/codec.dart'; 6 | import 'package:codable_dart/src/common/object.dart'; 7 | 8 | const Codec csv = CodableCodec(_CsvDelegate(), ObjectCodable()); 9 | 10 | class _CsvDelegate extends CodableCodecDelegate { 11 | const _CsvDelegate(); 12 | 13 | @override 14 | T decode(String input, Decodable using) => CsvDecoder.decode(input, using); 15 | 16 | @override 17 | String encode(T input, Encodable using) => CsvEncoder.encode(input, using: using); 18 | 19 | @override 20 | CodableCodecDelegate? fuse(Codec other) { 21 | if (other is Utf8Codec) { 22 | return const _CsvBytesDelegate() as CodableCodecDelegate; 23 | } 24 | return null; 25 | } 26 | } 27 | 28 | class _CsvBytesDelegate extends CodableCodecDelegate> { 29 | const _CsvBytesDelegate(); 30 | 31 | @override 32 | T decode(List input, Decodable using) => CsvDecoder.decodeBytes(input, using); 33 | 34 | @override 35 | List encode(T input, Encodable using) => CsvEncoder.encodeBytes(input, using: using); 36 | } 37 | -------------------------------------------------------------------------------- /test/hooks/proxy_hook.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | import 'package:codable_dart/extended.dart'; 3 | 4 | import 'delegate.dart'; 5 | 6 | abstract mixin class ProxyHook implements Hook { 7 | @override 8 | T decode(Decoder decoder, Decodable decodable) { 9 | return decodable.decode(ProxyDecoder(decoder, this)); 10 | } 11 | 12 | String visitString(String value) => value; 13 | String? visitStringOrNull(String? value) => value; 14 | 15 | @override 16 | void encode(T value, Encoder encoder, Encodable encodable) { 17 | encoder.encodeObject(value, using: encodable); 18 | } 19 | } 20 | 21 | class ProxyDecoder extends RecursiveDelegatingDecoder { 22 | ProxyDecoder(super.wrapped, this.hook); 23 | 24 | final ProxyHook hook; 25 | 26 | @override 27 | String decodeString() { 28 | return hook.visitString(super.decodeString()); 29 | } 30 | 31 | @override 32 | String? decodeStringOrNull() { 33 | return hook.visitStringOrNull(super.decodeStringOrNull()); 34 | } 35 | 36 | @override 37 | Decoder clone() { 38 | return ProxyDecoder(delegate.clone(), hook); 39 | } 40 | 41 | @override 42 | ProxyDecoder wrap(Decoder decoder) { 43 | return ProxyDecoder(decoder, hook); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/polymorphism/multi_interface/multi_interface_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/standard.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'model/material.dart'; 5 | 6 | final woodMap = {'type': 'wood'}; 7 | final ironMap = {'type': 'iron', 'symbol': 'Fe'}; 8 | final goldMap = {'type': 'gold', 'symbol': 'Au'}; 9 | final heliumMap = {'symbol': 'He'}; 10 | 11 | void main() { 12 | group('multi polymorphism', () { 13 | test('decodes single discriminated subtype', () { 14 | Material material = Material.decodable.fromMap(woodMap); 15 | expect(material, isA()); 16 | 17 | PeriodicElement element = PeriodicElement.decodable.fromMap(heliumMap); 18 | expect(element, isA()); 19 | }); 20 | 21 | test('decodes multi discriminated subtype', () { 22 | Material material = Material.decodable.fromMap(ironMap); 23 | PeriodicElement element = PeriodicElement.decodable.fromMap(ironMap); 24 | expect(material, isA()); 25 | expect(element, isA()); 26 | 27 | Material material2 = Material.decodable.fromMap(goldMap); 28 | PeriodicElement element2 = PeriodicElement.decodable.fromMap(goldMap); 29 | expect(material2, isA()); 30 | expect(element2, isA()); 31 | }); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /test/enum/enum_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/standard.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'model/color.dart'; 5 | 6 | void main() { 7 | group('enums', () { 8 | test('decode from string', () { 9 | final Color decoded = Color.codable.fromValue('green'); 10 | expect(decoded, Color.green); 11 | }); 12 | 13 | test('encode to string', () { 14 | final Object? encoded = Color.green.toValue(); 15 | expect(encoded, isA()); 16 | expect(encoded, 'green'); 17 | }); 18 | 19 | test('decode from null', () { 20 | final Color decoded = Color.codable.fromValue(null); 21 | expect(decoded, Color.none); 22 | }); 23 | 24 | test('encode to null', () { 25 | final Object? encoded = Color.none.toValue(); 26 | expect(encoded, isNull); 27 | }); 28 | 29 | test('decode from int', () { 30 | final Color decoded = StandardDecoder.decode(1, using: Color.codable, isHumanReadable: false); 31 | expect(decoded, Color.blue); 32 | }); 33 | 34 | test('encode to int', () { 35 | final Object? encoded = StandardEncoder.encode(Color.blue, using: Color.codable, isHumanReadable: false); 36 | expect(encoded, isA()); 37 | expect(encoded, 1); 38 | }); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /doc/enums.md: -------------------------------------------------------------------------------- 1 | Enums work exactly as normal models work. Therefore, an enum model should implement `SelfEncodable` and define a static `Codable codable` member. 2 | 3 | For example, a [`Color`](https://github.com/schultek/codable/blob/main/test/enum/model/color.dart) enum can be implemented like this: 4 | 5 | ```dart 6 | /// Enums should define static [Codable]s and implement [SelfEncodable] just like normal classes. 7 | enum Color implements SelfEncodable { 8 | green, 9 | blue, 10 | red; 11 | 12 | static const Codable codable = ColorCodable(); 13 | 14 | @override 15 | void encode(Encoder encoder) { 16 | encoder.encodeString(this.name); 17 | } 18 | } 19 | 20 | class ColorCodable extends SelfCodable { 21 | const ColorCodable(); 22 | 23 | @override 24 | Color decode(Decoder decoder) { 25 | return switch (decoder.whatsNext()) { 26 | DecodingType.string || DecodingType.unknown => switch (decoder.decodeString()) { 27 | 'green' => Color.green, 28 | 'blue' => Color.blue, 29 | 'red' => Color.red, 30 | // Throw an error on any unknown value. We could also choose a default value here as well. 31 | _ => decoder.expect('Color of green, blue or red'), 32 | }, 33 | _ => decoder.expect('Color as string'), 34 | }; 35 | } 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /test/polymorphism/basic/poly_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/standard.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'model/pet.dart'; 5 | 6 | final dogMap = {'name': 'Jasper', 'breed': 'Australian Shepherd', 'type': 'dog'}; 7 | final catMap = {'name': 'Whiskers', 'lives': 5, 'type': 'cat'}; 8 | final birdMap = {'color': 'red', 'type': 'bird'}; 9 | 10 | void main() { 11 | group('polymorphism', () { 12 | test('decodes explicit subtype', () { 13 | Dog dog = Dog.codable.fromMap(dogMap); 14 | expect(dog.name, 'Jasper'); 15 | }); 16 | 17 | test('encodes explicit subtype', () { 18 | Dog dog = Dog(name: 'Jasper', breed: 'Australian Shepherd'); 19 | Map map = dog.toMap(); 20 | expect(map, dogMap); 21 | }); 22 | 23 | test('decodes discriminated subtype', () { 24 | Pet pet = Pet.codable.fromMap(dogMap); 25 | expect(pet, isA()); 26 | expect((pet as Dog).name, "Jasper"); 27 | }); 28 | 29 | test('encodes base type', () { 30 | Pet pet = Dog(name: 'Jasper', breed: 'Australian Shepherd'); 31 | Map map = pet.toMap(); 32 | expect(map, dogMap); 33 | }); 34 | 35 | test('decodes default on unknown key', () { 36 | Pet pet = Pet.codable.fromMap(birdMap); 37 | expect(pet.runtimeType, Pet); 38 | expect(pet.type, 'bird'); 39 | }); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /codable_builder/README.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | TODO: Put a short description of the package here that helps potential users 15 | know whether this package might be useful for them. 16 | 17 | ## Features 18 | 19 | TODO: List what your package can do. Maybe include images, gifs, or videos. 20 | 21 | ## Getting started 22 | 23 | TODO: List prerequisites and provide or point to information on how to 24 | start using the package. 25 | 26 | ## Usage 27 | 28 | TODO: Include short and useful examples for package users. Add longer examples 29 | to `/example` folder. 30 | 31 | ```dart 32 | const like = 'sample'; 33 | ``` 34 | 35 | ## Additional information 36 | 37 | TODO: Tell users more about the package: where to find more information, how to 38 | contribute to the package, how to file issues, what response they can expect 39 | from the package authors, and more. 40 | -------------------------------------------------------------------------------- /lib/src/codec/standard.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert' hide json; 2 | 3 | import 'package:codable_dart/core.dart'; 4 | import 'package:codable_dart/src/codec/codec.dart'; 5 | import 'package:codable_dart/standard.dart'; 6 | 7 | import '../../json.dart'; 8 | 9 | 10 | class _StandardCodableCodec> extends CodableCodec { 11 | const _StandardCodableCodec(C codable) : super(const _StandardDelegate(), codable); 12 | 13 | @override 14 | Codec fuse(Codec other) { 15 | if (other is JsonCodec) { 16 | return (json as CodableCodec).fuseCodable(codable) as Codec; 17 | } else if (other is CodableCompatibleCodec) { 18 | return other.fuseCodable(codable) ?? super.fuse(other); 19 | } else { 20 | return super.fuse(other); 21 | } 22 | } 23 | } 24 | 25 | class _StandardDelegate extends CodableCodecDelegate { 26 | const _StandardDelegate(); 27 | 28 | @override 29 | T decode(Object? input, Decodable using) => StandardDecoder.decode(input, using: using); 30 | 31 | @override 32 | Object? encode(T input, Encodable using) => StandardEncoder.encode(input, using: using); 33 | } 34 | 35 | abstract interface class CodableCompatibleCodec implements Codec { 36 | Codec? fuseCodable(Codable codable); 37 | } 38 | 39 | extension StandardCodec on Codable { 40 | Codec get codec => _StandardCodableCodec>(this); 41 | } 42 | -------------------------------------------------------------------------------- /doc/types.md: -------------------------------------------------------------------------------- 1 | Some common types that have codables included in the package. 2 | 3 | # DateTime 4 | 5 | The `DateTimeCodable` is a codable implementation for the core `DateTime` type. It shows how to de/encode a core type and combines several encoding strategies. 6 | 7 | 1. If the data format supports `DateTime` as a [custom type](#custom-types), it is encoded as a custom scalar value. 8 | 2. It uses a custom configuration option `preferredFormat` which the user can use to specify one of three formats. 9 | 10 | - `iso8601` will encode the value as an ISO8601 `String`. 11 | - `unixMilliseconds` will encode the value as a unix milliseconds `int`. 12 | - `auto` will let the data format determine how the value is encoded. For human-readable formats it is encoded as a `String` and for others as an `int`. 13 | 14 | 3. It uses a custom configuration option `convertUtc` which controls whether the date value will be converted to UTC before encoding and to local time when decoding. 15 | 16 | The codable is implemented in [lib/src/common/datetime.dart](https://github.com/schultek/codable/blob/main/lib/src/common/datetime.dart) 17 | 18 | # Uri 19 | 20 | The `UriCodable` is a codable implementation for the core `Uri` type. 21 | 22 | If the data format supports `Uri` as a [custom type](#custom-types), the value is encoded as a custom scalar value. Else, it is encoded as a `String`. 23 | 24 | The codable is implemented in [lib/src/common/uri.dart](https://github.com/schultek/codable/blob/main/lib/src/common/uri.dart) 25 | -------------------------------------------------------------------------------- /lib/src/extended/lazy.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | 3 | abstract class LazyDecoder { 4 | void whatsNext(void Function(DecodingType type) onType); 5 | 6 | void decodeEager(void Function(Decoder decoder) onDecode); 7 | 8 | void decodeObject(void Function(T value) onValue, {Decodable? using}); 9 | void decodeObjectOrNull(void Function(T? value) onValue, {Decodable? using}); 10 | 11 | void decodeIterated(void Function(LazyIteratedDecoder decoder) onItem, {required void Function() done}); 12 | 13 | void decodeKeyed(void Function(Object /* String | int */ key, LazyKeyedDecoder decoder) onEntry, 14 | {required void Function() done}); 15 | 16 | void decodeList(void Function(List value) onValue, {Decodable? using}); 17 | } 18 | 19 | abstract class LazyKeyedDecoder implements LazyDecoder { 20 | void skipCurrentValue(); 21 | } 22 | 23 | abstract class LazyIteratedDecoder implements LazyDecoder { 24 | /// Skips the current item in the collection. 25 | /// 26 | /// This is useful when the [Decodable] implementation is not interested in the current item. 27 | /// It must be called before calling [nextItem] again if no decoding method is called instead. 28 | void skipCurrentItem(); 29 | } 30 | 31 | abstract class LazyDecodable implements Decodable { 32 | void decodeLazy(LazyDecoder decoder, void Function(T value) resolve); 33 | } 34 | 35 | abstract class LazyCodable extends Codable implements LazyDecodable { 36 | const LazyCodable(); 37 | } 38 | -------------------------------------------------------------------------------- /test/generics/basic/model/box.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | 3 | class Box implements SelfEncodable { 4 | Box(this.label, this.data); 5 | 6 | final String label; 7 | final T data; 8 | 9 | static Codable> codable([Codable? codable]) => BoxCodable(codable); 10 | 11 | @override 12 | void encode(Encoder encoder, [Encodable? encodableT]) { 13 | encoder.encodeKeyed() 14 | ..encodeString('label', label) 15 | ..encodeObject('data', data, using: encodableT) 16 | ..end(); 17 | } 18 | } 19 | 20 | extension BoxEncodableExtension on Box { 21 | SelfEncodable use([Encodable? encodableT]) { 22 | return SelfEncodable.fromHandler((e) => encode(e, encodableT)); 23 | } 24 | } 25 | 26 | class BoxCodable extends Codable> { 27 | const BoxCodable([Codable? codableT]) 28 | : encodableT = codableT, 29 | decodableT = codableT; 30 | 31 | const BoxCodable.of({this.encodableT, this.decodableT}); 32 | 33 | final Encodable? encodableT; 34 | final Decodable? decodableT; 35 | 36 | @override 37 | void encode(Box value, Encoder encoder) { 38 | value.encode(encoder, encodableT); 39 | } 40 | 41 | @override 42 | Box decode(Decoder decoder) { 43 | // For simplicity, we don't check the decoder.whatsNext() here. Don't do this for real implementations. 44 | final mapped = decoder.decodeMapped(); 45 | return Box( 46 | mapped.decodeString('label'), 47 | mapped.decodeObject('data', using: decodableT), 48 | ); 49 | } 50 | } -------------------------------------------------------------------------------- /lib/src/codec/json.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert' hide JsonDecoder, JsonEncoder; 3 | 4 | import 'package:codable_dart/core.dart'; 5 | import 'package:codable_dart/src/common/object.dart'; 6 | 7 | import '../../json.dart'; 8 | import 'codec.dart'; 9 | 10 | const Codec json = CodableCodec(_JsonDelegate(), ObjectCodable()); 11 | 12 | class _JsonDelegate extends CodableCodecDelegate { 13 | const _JsonDelegate(); 14 | 15 | @override 16 | T decode(String input, Decodable using) => JsonDecoder.decode(utf8.encode(input), using); 17 | 18 | @override 19 | String encode(T input, Encodable using) => utf8.decode(JsonEncoder.encode(input, using: using)); 20 | 21 | @override 22 | CodableCodecDelegate? fuse(Codec other) { 23 | if (other is Utf8Codec) { 24 | return const _JsonBytesDelegate() as CodableCodecDelegate; 25 | } 26 | return null; 27 | } 28 | } 29 | 30 | class _JsonBytesDelegate extends CodableCodecDelegate> { 31 | const _JsonBytesDelegate(); 32 | 33 | @override 34 | T decode(List input, Decodable using) => JsonDecoder.decode(input, using); 35 | 36 | @override 37 | List encode(T input, Encodable using) => JsonEncoder.encode(input, using: using); 38 | 39 | @override 40 | Sink> startChunkedConversion(Sink sink, Decodable decodable) { 41 | return JsonDecoder.decodeLazy( 42 | sink.add, 43 | decodable, 44 | onError: sink is EventSink ? (sink as EventSink).addError : null, 45 | onDone: sink.close, 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/mappable/simple.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | 3 | import 'mapper.dart'; 4 | 5 | abstract class SimpleMapper extends Mapper implements CodableMapper, Codable { 6 | const SimpleMapper(); 7 | 8 | @override 9 | Codable get codable => this; 10 | 11 | @override 12 | T decode(Decoder decoder); 13 | @override 14 | void encode(T value, Encoder encoder); 15 | } 16 | 17 | abstract class SimpleMapper1 extends Mapper implements CodableMapper1 { 18 | const SimpleMapper1(); 19 | 20 | @override 21 | Codable codable([Codable? codableA]) => Codable.fromHandlers( 22 | decode: (d) => decode(d, codableA), 23 | encode: (v, e) => encode(v, e, codableA), 24 | ); 25 | 26 | T decode(Decoder decoder, [Decodable? decodableA]); 27 | void encode(covariant T value, Encoder encoder, [Encodable? encodableA]); 28 | 29 | @override 30 | Function get typeFactory; 31 | } 32 | 33 | abstract class SimpleMapper2 extends Mapper implements CodableMapper2 { 34 | const SimpleMapper2(); 35 | 36 | @override 37 | Codable codable([Codable? codableA, Codable? codableB]) { 38 | return Codable.fromHandlers( 39 | decode: (d) => decode(d, codableA, codableB), 40 | encode: (v, e) => encode(v, e, codableA, codableB), 41 | ); 42 | } 43 | 44 | T decode(Decoder decoder, [Decodable? decodableA, Decodable? decodableB]); 45 | void encode(covariant T value, Encoder encoder, [Encodable? encodableA, Encodable? encodableB]); 46 | 47 | @override 48 | Function get typeFactory; 49 | } 50 | -------------------------------------------------------------------------------- /codable_builder/test/codable_member_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_builder/codable_builder.dart'; 2 | import 'package:code_builder/code_builder.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('codable member', () { 7 | final builder = const CodableBuilder(); 8 | final emitter = DartEmitter(useNullSafetySyntax: true); 9 | 10 | test('should build field for basic class', () { 11 | final code = builder.buildStaticCodableMember( 12 | CodableClassNode( 13 | name: 'TestClass', 14 | url: 'package:test/test_class.dart', 15 | isSelfEncodable: true, 16 | constructorName: 'TestClass', 17 | fields: [], 18 | ), 19 | ); 20 | 21 | expect( 22 | code.accept(emitter).toString(), 23 | equals('static const Codable codable = TestClassCodable();\n'), 24 | ); 25 | }); 26 | 27 | test('should build method for generic class', () { 28 | final code = builder.buildStaticCodableMember( 29 | CodableClassNode( 30 | name: 'TestClass', 31 | url: 'package:test/test_class.dart', 32 | typeParameters: [ 33 | CodableTypeParameter(name: 'T'), 34 | CodableTypeParameter(name: 'U'), 35 | ], 36 | isSelfEncodable: true, 37 | constructorName: 'TestClass', 38 | fields: [], 39 | ), 40 | ); 41 | 42 | expect( 43 | code.accept(emitter).toString(), 44 | equals( 45 | 'static Codable> codable([Codable? codableT, Codable? codableU, ]) { return TestClassCodable(codableT, codableU); } '), 46 | ); 47 | }); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /codable_builder/lib/src/builders/decode_mixin.dart: -------------------------------------------------------------------------------- 1 | part of 'codable_builder.dart'; 2 | 3 | abstract mixin class DecodeMixin implements CodableBuilder { 4 | String _buildDecodingInvocation(CodableFieldNode node, CodableClassNode clazz, {required bool includeKey}) { 5 | final suffix = node.type.isNullable ? 'OrNull' : ''; 6 | final name = switch (node.type) { 7 | ObjectCodableType t when t.using != null => 'decodeObject$suffix', 8 | ObjectCodableType t when t.name == 'String' => 'decodeString$suffix', 9 | ObjectCodableType t when t.name == 'int' => 'decodeInt$suffix', 10 | ObjectCodableType t when t.name == 'double' => 'decodeDouble$suffix', 11 | ObjectCodableType t when t.name == 'num' => 'decodeNum$suffix', 12 | ObjectCodableType t when t.name == 'bool' => 'decodeBool$suffix', 13 | ObjectCodableType() => 'decodeObject$suffix', 14 | ListCodableType() => 'decodeList$suffix', 15 | MapCodableType() => 'decodeMap$suffix', 16 | }; 17 | final args = switch (node.type) { 18 | ObjectCodableType t when t.using != null => ['using: ${t.using}'], 19 | ObjectCodableType t when clazz.typeParameters.any((tp) => tp.name == t.name) => ['using: decodable${t.name}'], 20 | ListCodableType t when t.itemType.using != null => ['using: ${t.itemType.using}'], 21 | MapCodableType t => [ 22 | if (t.keyType.using != null) 'keyUsing: ${t.keyType.using}', 23 | if (t.valueType.using != null) 'valueUsing: ${t.valueType.using}' 24 | ], 25 | _ => [], 26 | }; 27 | return '$name(${[ 28 | if (includeKey) '\'${node.key}\'', 29 | if (includeKey && node.id != null) 'id: ${node.id}', 30 | ...args 31 | ].join(', ')})'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /doc/codec.md: -------------------------------------------------------------------------------- 1 | The package also defines optional APIs for working with the `Codec` API from `dart:covert`. 2 | 3 | Any `Codable` can be converted to a `Codec` like this: 4 | 5 | ```dart 6 | // Import the 'standard' format, including the `.codec` extension. 7 | import 'package:codable_dart/standard.dart'; 8 | 9 | void main() { 10 | final Codec codec = Person.codable.codec; 11 | } 12 | ``` 13 | 14 | This gives the default codec to decode and encode a model using the 'standard' format, usually to a `Map` of its properties. 15 | 16 | Further working with different data formats is enabled by fusing other codecs: 17 | 18 | ```dart 19 | // Imports the core 'json' codec. 20 | import 'dart:convert'; 21 | // Imports the packages 'msgPack' codec. 22 | import 'package:codable_dart/msgpack.dart'; 23 | 24 | // Import the '.codec' extension. 25 | import 'package:codable_dart/standard.dart'; 26 | 27 | final Codec personToJsonCodec = Person.codable.codec.fuse(json); 28 | 29 | final Codec> personToMsgPackCodec = Person.codable.codec.fuse(msgPack); 30 | ``` 31 | 32 | Fusing codecs like this will be as performant as using the codable API directly, as it still optimizes out the intermediate `Map` allocation. 33 | 34 | --- 35 | 36 | Some formats, like JSON and CSV also supports fusing with the `utf8` codec for efficient binary de/encoding. 37 | 38 | ```dart 39 | final Codec personToJsonBytesCodec = Person.codable.codec.fuse(json).fuse(utf8); 40 | ``` 41 | 42 | --- 43 | 44 | The binary JSON codec also supports chunked decoding, given that the models `Codable` implements the `LazyDecodable` interface. 45 | 46 | ```dart 47 | final Stream> byteStream = ...; 48 | final Future personStream = Person.codable.codec.fuse(json).fuse(utf8).decoder.bind(byteStream).single; 49 | ``` 50 | 51 | -------------------------------------------------------------------------------- /test/progressive_json/model/circle.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | import 'package:codable_dart/src/extended/reference.dart'; 3 | 4 | class Circle implements SelfEncodable { 5 | Circle(this.radius, Reference center) { 6 | center.get((v) => this.center = v); 7 | } 8 | 9 | final int radius; 10 | late final Circle center; 11 | 12 | @override 13 | void encode(Encoder encoder) { 14 | encoder.encodeKeyed() 15 | ..encodeInt('radius', radius) 16 | ..encodeReference('center', center) 17 | ..end(); 18 | } 19 | } 20 | 21 | class CircleCodable extends SelfCodable { 22 | const CircleCodable(); 23 | 24 | @override 25 | Circle decode(Decoder decoder) { 26 | return switch (decoder.whatsNext()) { 27 | // If the format prefers mapped decoding, use mapped decoding. 28 | DecodingType.mapped || DecodingType.map => decodeMapped(decoder.decodeMapped()), 29 | // If the format prefers keyed decoding or is non-self describing, use keyed decoding. 30 | DecodingType.keyed || DecodingType.unknown => decodeKeyed(decoder.decodeKeyed()), 31 | _ => decoder.expect('mapped or keyed'), 32 | }; 33 | } 34 | 35 | Circle decodeKeyed(KeyedDecoder keyed) { 36 | late int radius; 37 | late Reference center; 38 | 39 | for (Object? key; (key = keyed.nextKey()) != null;) { 40 | switch (key) { 41 | case 'radius': 42 | radius = keyed.decodeInt(); 43 | case 'center': 44 | center = keyed.decodeReference(using: this); 45 | default: 46 | keyed.skipCurrentValue(); 47 | } 48 | } 49 | 50 | return Circle(radius, center); 51 | } 52 | 53 | Circle decodeMapped(MappedDecoder mapped) { 54 | return Circle( 55 | mapped.decodeInt('radius'), 56 | mapped.decodeReference('center', using: this), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/async/model/result.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | import 'package:codable_dart/extended.dart'; 3 | 4 | import '../../basic/model/person.dart'; 5 | 6 | sealed class UsersResult {} 7 | 8 | class UsersData extends UsersResult { 9 | UsersData(this.users); 10 | 11 | final List users; 12 | } 13 | 14 | class UsersStream extends UsersResult { 15 | UsersStream(this.users); 16 | 17 | final Stream users; 18 | } 19 | 20 | class UsersError extends UsersResult { 21 | UsersError(this.error); 22 | 23 | final Object? error; 24 | } 25 | 26 | class UsersResultCodable extends LazyCodable { 27 | const UsersResultCodable(); 28 | 29 | @override 30 | UsersResult decode(Decoder decoder) { 31 | final keyed = decoder.decodeKeyed(); 32 | 33 | List? users; 34 | Object? error; 35 | 36 | for (Object? key; (key = keyed.nextKey()) != null;) { 37 | switch (key) { 38 | case 'users': 39 | users = keyed.decodeList(using: Person.codable); 40 | case 'error': 41 | error = keyed.decodeObjectOrNull(); 42 | 43 | default: 44 | keyed.skipCurrentValue(); 45 | } 46 | } 47 | 48 | return users != null ? UsersData(users) : UsersError(error); 49 | } 50 | 51 | @override 52 | void decodeLazy(LazyDecoder decoder, void Function(UsersResult) resolve) { 53 | decoder.decodeKeyed((key, decoder) { 54 | switch (key) { 55 | case 'users': 56 | resolve(UsersStream(decoder.decodeStream(using: Person.codable))); 57 | case 'error': 58 | decoder.decodeObjectOrNull((e) => resolve(UsersError(e))); 59 | default: 60 | decoder.skipCurrentValue(); 61 | } 62 | }, done: () {}); 63 | } 64 | 65 | @override 66 | void encode(UsersResult value, Encoder encoder) { 67 | // TODO: implement encode 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /doc/formats.md: -------------------------------------------------------------------------------- 1 | Different data formats included in the package. 2 | 3 | # Standard 4 | 5 | The "standard" format de/encodes models to Dart `Map`s, `List`s and primitive value types. 6 | 7 | _This is the equivalent to what the `toJson()` method of `json_serializable` does._ As explained in the beginning, this technically is not serialization, but since its a very common thing to do, this protocol of course also has support for it. 8 | 9 | The format can be used by importing `package:codable_dart/standard.dart`, which provides the `fromValue()`, `toValue()` and `fromMap()`, `toMap()` extension methods. 10 | 11 | # JSON 12 | 13 | An implementation for JSON, a human-readable self-describing serial data format. 14 | 15 | This supports de/encoding models to both a `String` as well as a `List` of bytes. 16 | 17 | The format can used by importing `package:codable_dart/json.dart` and provides the `fromJson()`, `toJson()` and `fromJsonBytes()`, `toJsonBytes()` extension methods. 18 | 19 | _The implementation is largely based on the [`crimson`](https://pub.dev/packages/crimson) package.\_ 20 | 21 | # CSV 22 | 23 | An implementation for CSV serialization, a human-readable non-self-describing serial data format. 24 | 25 | This is limited to simple values only, no nested objects or lists. Values are separated by ",". 26 | 27 | Different to other formats, this implementation operates exclusively on lists of models, since all CSV data consists of a number of rows. 28 | 29 | The format can used by importing `package:codable_dart/csv.dart` and provides the `fromCsv()`, `toCsv()` and `fromCsvBytes()`, `toCsvBytes()` extension methods. 30 | 31 | # MessagePack 32 | 33 | An implementation for MessagePack, a binary self-describing serial data format. 34 | 35 | The format can used by importing `package:codable_dart/json.dart` and provides the `fromMsgPack()` and `toMsgPack()` extension methods. 36 | 37 | _The implementation is largely based on the [`messagepack`](https://pub.dev/packages/messagepack) package.\_ 38 | -------------------------------------------------------------------------------- /test/basic/test_data.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | final personTestData = { 4 | "name": "Alice Smith", 5 | "age": 30, 6 | "height": 5.6, 7 | "isDeveloper": true, 8 | "parent": { 9 | "name": "Carol Smith", 10 | "age": 55, 11 | "height": 5.4, 12 | "isDeveloper": false, 13 | "parent": null, 14 | "hobbies": ["gardening", "reading"], 15 | "friends": [] 16 | }, 17 | "hobbies": ["coding", "hiking", "painting"], 18 | "friends": [ 19 | { 20 | "name": "Bob Johnson", 21 | "age": 32, 22 | "height": 5.9, 23 | "isDeveloper": true, 24 | "parent": { 25 | "name": "David Johnson", 26 | "age": 60, 27 | "height": 6.0, 28 | "isDeveloper": false, 29 | "parent": null, 30 | "hobbies": ["woodworking"], 31 | "friends": [] 32 | }, 33 | "hobbies": ["gaming", "cycling"], 34 | "friends": [] 35 | }, 36 | { 37 | "name": "Eve Davis", 38 | "age": 28, 39 | "height": 5.5, 40 | "isDeveloper": false, 41 | "parent": null, 42 | "hobbies": ["dancing", "photography"], 43 | "friends": [] 44 | } 45 | ] 46 | }; 47 | 48 | final personTestJson = jsonEncode(personTestData); 49 | final personTestJsonBytes = utf8.encode(personTestJson); 50 | 51 | final personListTestJson = jsonEncode(List.filled(10, personTestData)); 52 | final personListTestJsonBytes = utf8.encode(personListTestJson); 53 | 54 | // https://msgpack.org/ 55 | final personTestMsgpackBytes = base64Decode( 56 | 'h6RuYW1lq0FsaWNlIFNtaXRoo2FnZR6maGVpZ2h0y0AWZmZmZmZmq2lzRGV2ZWxvcGVyw6ZwYXJlbnSHpG5hbWWrQ2Fyb2wgU21pdGijYWdlN6ZoZWlnaHTLQBWZmZmZmZqraXNEZXZlbG9wZXLCpnBhcmVudMCnaG9iYmllc5KpZ2FyZGVuaW5np3JlYWRpbmenZnJpZW5kc5CnaG9iYmllc5OmY29kaW5npmhpa2luZ6hwYWludGluZ6dmcmllbmRzkoekbmFtZatCb2IgSm9obnNvbqNhZ2UgpmhlaWdodMtAF5mZmZmZmqtpc0RldmVsb3BlcsOmcGFyZW50h6RuYW1lrURhdmlkIEpvaG5zb26jYWdlPKZoZWlnaHQGq2lzRGV2ZWxvcGVywqZwYXJlbnTAp2hvYmJpZXORq3dvb2R3b3JraW5np2ZyaWVuZHOQp2hvYmJpZXOSpmdhbWluZ6djeWNsaW5np2ZyaWVuZHOQh6RuYW1lqUV2ZSBEYXZpc6NhZ2UcpmhlaWdodMtAFgAAAAAAAKtpc0RldmVsb3BlcsKmcGFyZW50wKdob2JiaWVzkqdkYW5jaW5nq3Bob3RvZ3JhcGh5p2ZyaWVuZHOQ'); 57 | -------------------------------------------------------------------------------- /test/csv/test_data.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'model/measures.dart'; 4 | 5 | final measuresCsv = ''' 6 | id,name,age,isActive,signupDate,website 7 | 1,John Doe,25,true,2023-06-15T00:00:00.000,https://johndoe.com 8 | 2,Jane Smith,30,false,,https://janesmith.org 9 | 3,,45,true,2021-09-12T00:00:00.000,https://example.com 10 | 4,Alex Brown,29,false,2020-11-23T00:00:00.000, 11 | 5,Chris Johnson,34,true,2019-03-10T00:00:00.000,https://chrisjohnson.net 12 | '''; 13 | 14 | final measuresCsvBytes = utf8.encode(measuresCsv); 15 | 16 | final measuresObjects = [ 17 | Measures('1', 'John Doe', 25, true, DateTime(2023, 6, 15), 18 | Uri.parse('https://johndoe.com')), 19 | Measures( 20 | '2', 'Jane Smith', 30, false, null, Uri.parse('https://janesmith.org')), 21 | Measures('3', null, 45, true, DateTime(2021, 9, 12), 22 | Uri.parse('https://example.com')), 23 | Measures('4', 'Alex Brown', 29, false, DateTime(2020, 11, 23), null), 24 | Measures('5', 'Chris Johnson', 34, true, DateTime(2019, 3, 10), 25 | Uri.parse('https://chrisjohnson.net')), 26 | ]; 27 | 28 | final measuresData = [ 29 | { 30 | "id": "1", 31 | "name": "John Doe", 32 | "age": 25, 33 | "isActive": true, 34 | "signupDate": "2023-06-15T00:00:00.000", 35 | "website": "https://johndoe.com" 36 | }, 37 | { 38 | "id": "2", 39 | "name": "Jane Smith", 40 | "age": 30, 41 | "isActive": false, 42 | "signupDate": null, 43 | "website": "https://janesmith.org" 44 | }, 45 | { 46 | "id": "3", 47 | "name": null, 48 | "age": 45, 49 | "isActive": true, 50 | "signupDate": "2021-09-12T00:00:00.000", 51 | "website": "https://example.com" 52 | }, 53 | { 54 | "id": "4", 55 | "name": "Alex Brown", 56 | "age": 29, 57 | "isActive": false, 58 | "signupDate": "2020-11-23T00:00:00.000", 59 | "website": null, 60 | }, 61 | { 62 | "id": "5", 63 | "name": "Chris Johnson", 64 | "age": 34, 65 | "isActive": true, 66 | "signupDate": "2019-03-10T00:00:00.000", 67 | "website": "https://chrisjohnson.net" 68 | } 69 | ]; 70 | 71 | final measuresJson = jsonEncode(measuresData); 72 | -------------------------------------------------------------------------------- /lib/src/extended/generics.dart: -------------------------------------------------------------------------------- 1 | /// This file contains interfaces and helper utilities to work with generic types. 2 | library generic; 3 | 4 | import 'package:codable_dart/core.dart'; 5 | 6 | /// Interface for a [Decodable] that wraps another [Decodable] of the same (or similar) type. 7 | abstract interface class ComposedDecodable implements Decodable { 8 | /// Unwraps the inner [Decodable] and passes it to the provided function [fn]. 9 | R unwrap(R Function(Decodable? codable) fn); 10 | } 11 | 12 | // ============================== 13 | // Generics with 1 type parameter 14 | // ============================== 15 | 16 | /// Interface for a [Decodable] of a generic type T, that is composed of another [Decodable] for A. 17 | abstract interface class ComposedDecodable1 implements Decodable { 18 | /// Extracts the child [Decodable] for the type parameter A from the composed [Decodable] for type T. 19 | R extract(R Function(Decodable? decodableA) fn); 20 | } 21 | 22 | // =============================== 23 | // Generics with 2 type parameters 24 | // =============================== 25 | 26 | /// Interface for a [Decodable] of a generic type T, that is composed of other [Decodable]s for A and B. 27 | abstract interface class ComposedDecodable2 implements Decodable { 28 | /// Extracts the child [Decodable]s for the type parameters A and B from the composed [Decodable] 29 | /// for type T. 30 | R extract(R Function(Decodable? decodableA, Decodable? decodableB) fn); 31 | } 32 | 33 | extension ExtractDecodable on Decodable? { 34 | R extractArg1(R Function(Decodable? decodableA) fn) { 35 | if (this case ComposedDecodable d) { 36 | return d.unwrap((c) => c.extractArg1(fn)); 37 | } 38 | if (this case ComposedDecodable1 d) { 39 | return d.extract(fn); 40 | } 41 | return fn(null); 42 | } 43 | 44 | R extractArg2(R Function(Decodable? decodableA, Decodable? decodableB) fn) { 45 | if (this case ComposedDecodable d) { 46 | return d.unwrap((c) => c.extractArg2(fn)); 47 | } 48 | if (this case ComposedDecodable2 d) { 49 | return d.extract(fn); 50 | } 51 | return fn(null, null); 52 | } 53 | } -------------------------------------------------------------------------------- /lib/src/codec/codec.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:codable_dart/standard.dart'; 4 | 5 | import '../../core.dart'; 6 | 7 | abstract class CodableCodecDelegate { 8 | const CodableCodecDelegate(); 9 | 10 | T decode(Out input, Decodable using); 11 | Out encode(T input, Encodable using); 12 | 13 | Sink startChunkedConversion(Sink sink, Decodable decodable) { 14 | throw UnsupportedError( 15 | "This codable does not support chunked conversions: $this", 16 | ); 17 | } 18 | 19 | CodableCodecDelegate? fuse(Codec other) { 20 | return null; 21 | } 22 | } 23 | 24 | class CodableCodec extends Codec implements CodableCompatibleCodec { 25 | const CodableCodec(this.delegate, this.codable); 26 | 27 | final CodableCodecDelegate delegate; 28 | final Codable codable; 29 | 30 | @override 31 | Converter get decoder => _CodableDecoder(delegate, codable); 32 | 33 | @override 34 | Converter get encoder => _CodableEncoder(delegate, codable); 35 | 36 | @override 37 | Codec? fuseCodable(Codable codable) { 38 | return CodableCodec(delegate, codable); 39 | } 40 | 41 | @override 42 | Codec fuse(Codec other) { 43 | final d2 = delegate.fuse(other); 44 | if (d2 != null) { 45 | return CodableCodec(d2, codable); 46 | } else { 47 | return super.fuse(other); 48 | } 49 | } 50 | } 51 | 52 | class _CodableDecoder extends Converter { 53 | const _CodableDecoder(this.delegate, this.decodable); 54 | 55 | final CodableCodecDelegate delegate; 56 | final Decodable decodable; 57 | 58 | @override 59 | In convert(Out input) => delegate.decode(input, decodable); 60 | 61 | @override 62 | Sink startChunkedConversion(Sink sink) { 63 | return delegate.startChunkedConversion(sink, decodable); 64 | } 65 | } 66 | 67 | class _CodableEncoder extends Converter { 68 | const _CodableEncoder(this.delegate, this.encodable); 69 | 70 | final CodableCodecDelegate delegate; 71 | final Encodable encodable; 72 | 73 | @override 74 | Out convert(In input) => delegate.encode(input, encodable); 75 | } 76 | -------------------------------------------------------------------------------- /test/benchmark/bench.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | 3 | import '../utils.dart' show CallbackSink; 4 | 5 | void compare( 6 | String name, { 7 | required void Function() self, 8 | required void Function()? other, 9 | }) { 10 | test(name, () { 11 | print('== $name =='); 12 | bench('codable', self); 13 | if (other != null) { 14 | bench('baseline', other); 15 | } 16 | }); 17 | } 18 | 19 | Future benchAsync(String name, Future Function() f, {int times = 10, bool sum = false}) async { 20 | for (var i = 0; i < times / 2; i++) { 21 | await f(); 22 | } 23 | final s = Stopwatch()..start(); 24 | for (var i = 0; i < times; i++) { 25 | await f(); 26 | } 27 | s.stop(); 28 | var time = formatTime(s.elapsedMicroseconds ~/ (sum ? 1 : times)); 29 | print('$name: $time'); 30 | } 31 | 32 | Future benchSink(String name, Future Function(Sink Function(Sink)) run, {int times = 10}) async { 33 | for (var i = 0; i < times / 2; i++) { 34 | await run((s) => s); 35 | } 36 | final s = Stopwatch()..start(); 37 | final sc = Stopwatch()..start(); 38 | int t = 0, l = 0, n = 0; 39 | await run((sink) { 40 | return CallbackSink((value) { 41 | sc.reset(); 42 | sink.add(value); 43 | t += (l = sc.elapsedMicroseconds); 44 | n++; 45 | }, () { 46 | sc.reset(); 47 | sink.close(); 48 | sc.stop(); 49 | if (n > 0) { 50 | print('$name (avg chunk of $n): ${formatTime(t ~/ n)}'); 51 | print('$name (last chunk): ${formatTime(l)}'); 52 | } 53 | print('$name (closing): ${formatTime(sc.elapsedMicroseconds)}'); 54 | }); 55 | }); 56 | s.stop(); 57 | var time = formatTime(s.elapsedMicroseconds); 58 | print('$name: $time'); 59 | } 60 | 61 | void bench(String name, void Function() f, {int times = 10, bool sum = false}) { 62 | for (var i = 0; i < times / 2; i++) { 63 | f(); 64 | } 65 | final s = Stopwatch()..start(); 66 | for (var i = 0; i < times; i++) { 67 | f(); 68 | } 69 | s.stop(); 70 | var time = formatTime(s.elapsedMicroseconds ~/ (sum ? 1 : times)); 71 | print('$name: $time'); 72 | } 73 | 74 | String formatTime(int microseconds) { 75 | if (microseconds < 5000) { 76 | return '$microsecondsµs'; 77 | } else if (microseconds < 1000000) { 78 | return '${microseconds / 1000}ms'; 79 | } else { 80 | return '${microseconds / 1000000}s'; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /codable_builder/lib/src/models/types.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core'; 2 | import 'dart:core' as core; 3 | 4 | import 'package:code_builder/code_builder.dart'; 5 | 6 | import 'urls.dart'; 7 | 8 | sealed class CodableType { 9 | const CodableType(this.isNullable, [this.using]); 10 | 11 | final core.bool isNullable; 12 | final String? using; 13 | 14 | Reference get reference; 15 | 16 | static const CodableType string = ObjectCodableType.core('String'); 17 | static const CodableType stringNullable = ObjectCodableType.core('String', true); 18 | static const CodableType int = ObjectCodableType.core('int'); 19 | static const CodableType intNullable = ObjectCodableType.core('int', true); 20 | static const CodableType double = ObjectCodableType.core('double'); 21 | static const CodableType doubleNullable = ObjectCodableType.core('double', true); 22 | static const CodableType num = ObjectCodableType.core('num'); 23 | static const CodableType numNullable = ObjectCodableType.core('num', true); 24 | static const CodableType bool = ObjectCodableType.core('bool'); 25 | static const CodableType boolNullable = ObjectCodableType.core('bool', true); 26 | static const CodableType object = ObjectCodableType.core('Object'); 27 | static const CodableType objectNullable = ObjectCodableType.core('Object', true); 28 | } 29 | 30 | class ObjectCodableType extends CodableType { 31 | final String name; 32 | final String? url; 33 | 34 | const ObjectCodableType(this.name, [this.url, super.isNullable = false, super.using]); 35 | 36 | const ObjectCodableType.core(this.name, [super.isNullable = false]) : url = dartCoreUrl; 37 | 38 | @override 39 | Reference get reference => refer(name, url); 40 | } 41 | 42 | class ListCodableType extends CodableType { 43 | final CodableType itemType; 44 | 45 | const ListCodableType(this.itemType, super.isNullable); 46 | 47 | @override 48 | Reference get reference => TypeReference((b) { 49 | b.symbol = 'List'; 50 | b.url = dartCoreUrl; 51 | b.types.add(itemType.reference); 52 | }); 53 | } 54 | 55 | class MapCodableType extends CodableType { 56 | final CodableType keyType; 57 | final CodableType valueType; 58 | 59 | const MapCodableType(this.keyType, this.valueType, super.isNullable); 60 | 61 | @override 62 | Reference get reference => TypeReference((b) { 63 | b.symbol = 'Map'; 64 | b.url = dartCoreUrl; 65 | b.types.addAll([keyType.reference, valueType.reference]); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /test/polymorphism/multi_interface/model/material.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | import 'package:codable_dart/extended.dart'; 3 | 4 | abstract class Material { 5 | Material(); 6 | 7 | static const Decodable decodable = MaterialDecodable(); 8 | } 9 | 10 | abstract class PeriodicElement { 11 | PeriodicElement(); 12 | 13 | static const Decodable decodable = PeriodicElementDecodable(); 14 | } 15 | 16 | class Wood extends Material { 17 | Wood(); 18 | } 19 | 20 | class Iron extends Material implements PeriodicElement { 21 | Iron(); 22 | } 23 | 24 | class Gold extends PeriodicElement implements Material { 25 | Gold(); 26 | } 27 | 28 | class Helium extends PeriodicElement { 29 | Helium(); 30 | } 31 | 32 | // Decodable implementations 33 | // ---------------------- 34 | // For this test we only care about decoding, so we only create 35 | // Decodable implementations instead of Codable implementations. 36 | 37 | class MaterialDecodable with SuperDecodable { 38 | const MaterialDecodable(); 39 | 40 | @override 41 | String get discriminatorKey => 'type'; 42 | 43 | @override 44 | List> get discriminators => [ 45 | Discriminator('wood', WoodDecodable.new), 46 | Discriminator('iron', IronDecodable.new), 47 | Discriminator('gold', GoldDecodable.new), 48 | ]; 49 | } 50 | 51 | class PeriodicElementDecodable with SuperDecodable { 52 | const PeriodicElementDecodable(); 53 | 54 | @override 55 | String get discriminatorKey => 'symbol'; 56 | 57 | @override 58 | List> get discriminators => [ 59 | Discriminator('Fe', IronDecodable.new), 60 | Discriminator('Au', GoldDecodable.new), 61 | Discriminator('He', HeliumDecodable.new), 62 | ]; 63 | } 64 | 65 | class WoodDecodable implements Decodable { 66 | @override 67 | Wood decode(Decoder decoder) { 68 | return Wood(); 69 | } 70 | } 71 | 72 | class IronDecodable implements Decodable { 73 | @override 74 | Iron decode(Decoder decoder) { 75 | return Iron(); 76 | } 77 | } 78 | 79 | class GoldDecodable implements Decodable { 80 | @override 81 | Gold decode(Decoder decoder) { 82 | return Gold(); 83 | } 84 | } 85 | 86 | class HeliumDecodable implements Decodable { 87 | @override 88 | Helium decode(Decoder decoder) { 89 | return Helium(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/polymorphism/basic/model/pet.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | import 'package:codable_dart/extended.dart'; 3 | 4 | class Pet implements SelfEncodable { 5 | Pet({required this.type}); 6 | 7 | static const Codable codable = PetCodable(); 8 | 9 | final String type; 10 | 11 | @override 12 | void encode(Encoder encoder) { 13 | encoder.encodeKeyed() 14 | ..encodeString('type', type) 15 | ..end(); 16 | } 17 | } 18 | 19 | class Cat extends Pet { 20 | Cat({required this.name, this.lives = 7}) : super(type: 'cat'); 21 | 22 | static final Codable codable = CatCodable(); 23 | 24 | final String name; 25 | final int lives; 26 | 27 | @override 28 | void encode(Encoder encoder) { 29 | encoder.encodeKeyed() 30 | ..encodeString('type', type) 31 | ..encodeString('name', name) 32 | ..encodeInt('lives', lives) 33 | ..end(); 34 | } 35 | } 36 | 37 | class Dog extends Pet { 38 | Dog({required this.name, required this.breed}) : super(type: 'dog'); 39 | 40 | static const Codable codable = DogCodable(); 41 | 42 | final String name; 43 | final String breed; 44 | 45 | @override 46 | Object? encode(Encoder encoder) { 47 | return encoder.encodeKeyed() 48 | ..encodeString('type', type) 49 | ..encodeString('name', name) 50 | ..encodeString('breed', breed) 51 | ..end(); 52 | } 53 | } 54 | 55 | // Pet 56 | 57 | class PetCodable extends SelfCodable with SuperDecodable { 58 | const PetCodable(); 59 | 60 | @override 61 | String get discriminatorKey => 'type'; 62 | 63 | @override 64 | List> get discriminators => [ 65 | Discriminator('cat', CatCodable.new), 66 | Discriminator('dog', DogCodable.new), 67 | ]; 68 | 69 | @override 70 | Pet decodeFallback(Decoder decoder) { 71 | return Pet(type: decoder.decodeMapped().decodeString('type')); 72 | } 73 | } 74 | 75 | class CatCodable extends SelfCodable { 76 | const CatCodable(); 77 | 78 | @override 79 | Cat decode(Decoder decoder) { 80 | final keyed = decoder.decodeMapped(); 81 | return Cat( 82 | name: keyed.decodeString('name'), 83 | lives: keyed.decodeInt('lives'), 84 | ); 85 | } 86 | } 87 | 88 | class DogCodable extends SelfCodable { 89 | const DogCodable(); 90 | 91 | @override 92 | Dog decode(Decoder decoder) { 93 | final keyed = decoder.decodeMapped(); 94 | return Dog( 95 | name: keyed.decodeString('name'), 96 | breed: keyed.decodeString('breed'), 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /test/error_handling/error_handling_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | import 'package:codable_dart/standard.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | class Data { 6 | const Data(this.value); 7 | final Object? value; 8 | } 9 | 10 | void main() { 11 | group('error handling', () { 12 | test('throws unexpected type error on wrong token', () { 13 | expect( 14 | () => Decodable.fromHandler((decoder) { 15 | return Uri.parse(decoder.decodeString()); 16 | }).fromMap({}), 17 | throwsA(isA().having( 18 | (e) => e.message, 19 | 'message', 20 | 'Failed to decode Uri: Unexpected type: Expected String but got _Map.', 21 | )), 22 | ); 23 | }); 24 | 25 | test('throws unexpected type error on expect call', () { 26 | expect( 27 | () => Decodable.fromHandler((decoder) { 28 | return decoder.expect('String or int'); 29 | }).fromMap({}), 30 | throwsA(isA().having( 31 | (e) => e.message, 32 | 'message', 33 | 'Failed to decode DateTime: Unexpected type: Expected String or int but got _Map.', 34 | )), 35 | ); 36 | }); 37 | 38 | test('throws wrapped exception with decoding path', () { 39 | expect( 40 | () => Decodable.fromHandler((decoder) { 41 | return Data(decoder.decodeMapped().decodeObject( 42 | 'value', 43 | using: Decodable.fromHandler((decoder) { 44 | return Uri.parse(decoder.decodeMapped().decodeString('path')); 45 | }), 46 | )); 47 | }).fromMap({ 48 | 'value': {'path': 42} 49 | }), 50 | throwsA(isA().having( 51 | (e) => e.message, 52 | 'message', 53 | 'Failed to decode Data->["value"]->Uri->["path"]: Unexpected type: Expected String but got int.', 54 | )), 55 | ); 56 | 57 | expect( 58 | () => Decodable.fromHandler((decoder) { 59 | return Data(decoder.decodeMapped().decodeList( 60 | 'values', 61 | using: Decodable.fromHandler((decoder) { 62 | return Uri.parse(decoder.decodeMapped().decodeString('path')); 63 | }), 64 | )); 65 | }).fromMap({ 66 | 'values': [ 67 | {'path': 42} 68 | ] 69 | }), 70 | throwsA(isA().having( 71 | (e) => e.message, 72 | 'message', 73 | 'Failed to decode Data->["values"]->[0]->Uri->["path"]: Unexpected type: Expected String but got int.', 74 | )), 75 | ); 76 | }); 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/extended/hooks.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:codable_dart/core.dart'; 3 | 4 | import 'generics.dart'; 5 | 6 | extension CodableHookExtension on Codable { 7 | /// Returns a [Codable] that applies the provided [Hook] when encoding and decoding [T]. 8 | Codable hook(Hook hook) => CodableHook(this, hook); 9 | 10 | /// If hook is not null, returns a [Codable] that applies the provided [Hook] when encoding and decoding [T]. 11 | /// Else returns this. 12 | Codable maybeHook(Hook? hook) => hook == null ? this : CodableHook(this, hook); 13 | } 14 | 15 | /// An object that can be used to modify the encoding and decoding behavior of a type of data format. 16 | abstract mixin class Hook { 17 | /// Called before decoding a value of type [T] using the [decoder] and [decodable]. 18 | /// 19 | /// The implementation may modify the decoding process by wrapping the [decoder] or [decodable], or 20 | /// by providing a custom decoding implementation. 21 | /// 22 | /// To forward to the original implementation, call `super.decode(decoder, decodable)`. 23 | T decode(Decoder decoder, Decodable decodable) => decodable.decode(decoder); 24 | 25 | /// Called before encoding a value of type [T] using the [encoder] and [encodable]. 26 | /// 27 | /// The implementation may modify the encoding process by wrapping the [encoder] or [encodable], or 28 | /// by providing a custom encoding implementation. 29 | /// 30 | /// To forward to the original implementation, call `super.encode(value, encoder, encodable)`. 31 | void encode(T value, Encoder encoder, Encodable encodable) => encodable.encode(value, encoder); 32 | } 33 | 34 | class CodableHook implements Codable, ComposedDecodable { 35 | const CodableHook(this.codable, this.hook); 36 | 37 | final Codable codable; 38 | final Hook hook; 39 | 40 | @override 41 | T decode(Decoder decoder) { 42 | return hook.decode(decoder, codable); 43 | } 44 | 45 | @override 46 | void encode(T value, Encoder encoder) { 47 | hook.encode(value, encoder, codable); 48 | } 49 | 50 | @override 51 | R unwrap(R Function(Decodable? codable) fn) { 52 | return fn(codable); 53 | } 54 | } 55 | 56 | class CodableWithHooks implements Codable, ComposedDecodable { 57 | CodableWithHooks(Codable codable, Hook hook, [Hook? superHook]) : _codable = codable.hook(hook).maybeHook(superHook); 58 | 59 | CodableWithHooks.inherited(Codable codable, Hook hook) : _codable = codable.hook(hook); 60 | 61 | final Codable _codable; 62 | 63 | @override 64 | T decode(Decoder decoder) { 65 | return _codable.decode(decoder); 66 | } 67 | 68 | @override 69 | void encode(T value, Encoder encoder) { 70 | _codable.encode(value, encoder); 71 | } 72 | 73 | @override 74 | R unwrap(R Function(Decodable? codable) fn) { 75 | return fn(_codable); 76 | } 77 | } -------------------------------------------------------------------------------- /test/generics/basic/basic_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/json.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import '../../basic/model/person.dart'; 5 | import 'model/box.dart'; 6 | 7 | final boxStringJson = '{"label":"name","data":"John"}'; 8 | final boxIntJson = '{"label":"count","data":3}'; 9 | final boxPersonJson = 10 | '{"label":"person","data":{"name":"John","age":30,"height":5.6,"isDeveloper":true,"parent":null,"hobbies":[],"friends":[]}}'; 11 | 12 | void main() { 13 | group('generics', () { 14 | group('basic', () { 15 | test('decodes a box of dynamic', () { 16 | // By default, the codable for a generic class is dynamic. 17 | final Box decoded = Box.codable().fromJson(boxStringJson); 18 | expect(decoded.label, 'name'); 19 | expect(decoded.data, 'John'); 20 | }); 21 | 22 | test('encodes a box of dynamic', () { 23 | final Box box = Box('name', 'John'); 24 | final String encoded = box.toJson(); 25 | expect(encoded, boxStringJson); 26 | }); 27 | 28 | test('decodes a box of string', () { 29 | // The codable for a generic class can be explicitly set to a specific type. 30 | final Box decoded = Box.codable().fromJson(boxStringJson); 31 | expect(decoded.label, 'name'); 32 | expect(decoded.data, 'John'); 33 | }); 34 | 35 | test('encodes a box of string', () { 36 | final Box box = Box('name', 'John'); 37 | final String encoded = box.toJson(); 38 | expect(encoded, boxStringJson); 39 | }); 40 | 41 | test('decodes a box of int', () { 42 | // The codable for a generic class can be explicitly set to a specific type. 43 | final Box decoded = Box.codable().fromJson(boxIntJson); 44 | expect(decoded.label, 'count'); 45 | expect(decoded.data, 3); 46 | }); 47 | 48 | test('encodes a box of int', () { 49 | final Box box = Box('count', 3); 50 | final String encoded = box.toJson(); 51 | expect(encoded, boxIntJson); 52 | }); 53 | 54 | test('decodes a box of person', () { 55 | // For a non-primitive type, the child codable must be explicitly provided. 56 | final Box decoded = Box.codable(Person.codable).fromJson(boxPersonJson); 57 | expect(decoded.label, 'person'); 58 | expect(decoded.data, Person('John', 30, 5.6, true, null, [], [])); 59 | }); 60 | 61 | test('encodes a box of person', () { 62 | final Box box = Box('person', Person('John', 30, 5.6, true, null, [], [])); 63 | // For encoding a non-primitive type, the child codable must be explicitly provided. 64 | final String encoded = box.use(Person.codable).toJson(); 65 | expect(encoded, boxPersonJson); 66 | }); 67 | }); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /codable_builder/lib/src/builders/encode_mixin.dart: -------------------------------------------------------------------------------- 1 | part of 'codable_builder.dart'; 2 | 3 | abstract mixin class EncodeMixin implements CodableBuilder { 4 | @override 5 | Method buildSelfEncodeMethod(CodableClassNode node) { 6 | return Method((b) { 7 | b.name = 'encode'; 8 | b.returns = refer('void'); 9 | b.annotations.add(refer('override', dartCoreUrl)); 10 | b.requiredParameters.add(Parameter((b) { 11 | b.name = 'encoder'; 12 | b.type = refer('Encoder', codableCoreUrl); 13 | })); 14 | if (node.typeParameters.isNotEmpty) { 15 | b.optionalParameters.addAll(node.typeParameters.map((t) => Parameter((b) { 16 | b.name = 'encodable${t.name}'; 17 | b.type = TypeReference((b) { 18 | b.symbol = 'Encodable'; 19 | b.url = codableCoreUrl; 20 | b.types.add(refer(t.name)); 21 | b.isNullable = true; 22 | }); 23 | }))); 24 | } 25 | b.body = Block((b) { 26 | b.statements.add(Code(''' 27 | final keyed = encoder.encodeKeyed(); 28 | ${node.fields.map((f) => 'keyed.${_buildEncodingInvocation(f, node)};').join('\n')} 29 | keyed.end(); 30 | ''')); 31 | }); 32 | }); 33 | } 34 | 35 | String _buildEncodingInvocation(CodableFieldNode node, CodableClassNode clazz, {String? value}) { 36 | final suffix = node.type.isNullable ? 'OrNull' : ''; 37 | final name = switch (node.type) { 38 | ObjectCodableType t when t.using != null => 'encodeObject$suffix', 39 | ObjectCodableType t when t.name == 'String' => 'encodeString$suffix', 40 | ObjectCodableType t when t.name == 'int' => 'encodeInt$suffix', 41 | ObjectCodableType t when t.name == 'double' => 'encodeDouble$suffix', 42 | ObjectCodableType t when t.name == 'num' => 'encodeNum$suffix', 43 | ObjectCodableType t when t.name == 'bool' => 'encodeBool$suffix', 44 | ObjectCodableType() => 'encodeObject$suffix', 45 | ListCodableType() => 'encodeIterable$suffix', 46 | MapCodableType() => 'encodeMap$suffix', 47 | }; 48 | final args = switch (node.type) { 49 | ObjectCodableType t when t.using != null => ['using: ${t.using}'], 50 | ObjectCodableType t when clazz.typeParameters.any((tp) => tp.name == t.name) => ['using: encodable${t.name}'], 51 | ListCodableType t when t.itemType.using != null => ['using: ${t.itemType.using}'], 52 | MapCodableType t => [ 53 | if (t.keyType.using != null) 'keyUsing: ${t.keyType.using}', 54 | if (t.valueType.using != null) 'valueUsing: ${t.valueType.using}' 55 | ], 56 | _ => [], 57 | }; 58 | return '$name(${[ 59 | '\'${node.key}\'', 60 | '${value != null ? '$value.' : ''}${node.name}', 61 | if (node.id case int id) 'id: $id', 62 | ...args 63 | ].join(', ')})'; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/progressive_json/model/async_value.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:codable_dart/core.dart'; 4 | import 'package:codable_dart/extended.dart'; 5 | 6 | class AsyncValue implements SelfEncodable { 7 | AsyncValue.value(T value) : _value = value, _isRef = false; 8 | AsyncValue.reference(T value) : _value = value, _isRef = true; 9 | AsyncValue.future(Future future) : _value = future, _isRef = true { 10 | future.then((value) { 11 | _value = value; 12 | }); 13 | } 14 | 15 | FutureOr _value; 16 | final bool _isRef; 17 | 18 | bool get isPending => _value is Future; 19 | 20 | T get requireValue { 21 | if (_value is Future) { 22 | throw StateError('Cannot access requireValue on an AsyncValue that is still pending.'); 23 | } 24 | return _value as T; 25 | } 26 | 27 | FutureOr get value => _value; 28 | T? get valueOrNull => _value is Future ? null : _value as T?; 29 | 30 | @override 31 | void encode(Encoder encoder, [Encodable? encodableT]) { 32 | if (_value is Future) { 33 | encoder.encodeFuture(_value as Future, using: encodableT); 34 | } else if (_isRef) { 35 | encoder.encodeReference(_value as T, using: encodableT); 36 | } else { 37 | encoder.encodeObject(_value as T, using: encodableT); 38 | } 39 | } 40 | 41 | @override 42 | bool operator ==(Object other) { 43 | if (identical(this, other)) return true; 44 | if (other is! AsyncValue) return false; 45 | return _value == other._value; 46 | } 47 | 48 | @override 49 | int get hashCode => _value.hashCode; 50 | 51 | @override 52 | String toString() => isPending ? '[pending $T]' : _value.toString(); 53 | } 54 | 55 | 56 | extension AsyncEncodableExtension on AsyncValue { 57 | SelfEncodable use([Encodable? encodableT]) { 58 | return SelfEncodable.fromHandler((e) => encode(e, encodableT)); 59 | } 60 | } 61 | 62 | extension AsAsyncCodable on Codable { 63 | /// Returns a [Codable] that can encode and decode an [AsyncValue] of [T]. 64 | Codable> async() => AsyncCodable(this); 65 | } 66 | 67 | class AsyncCodable extends Codable> { 68 | const AsyncCodable([Codable? codableT]) 69 | : encodable = codableT, 70 | decodable = codableT; 71 | 72 | const AsyncCodable.of({this.encodable, this.decodable}); 73 | 74 | final Encodable? encodable; 75 | final Decodable? decodable; 76 | 77 | @override 78 | void encode(AsyncValue value, Encoder encoder) { 79 | value.encode(encoder, encodable); 80 | } 81 | 82 | @override 83 | AsyncValue decode(Decoder decoder) { 84 | final next = decoder.whatsNext(); 85 | if (next is DecodingType || next is DecodingType) { 86 | return AsyncValue.future(decoder.decodeFuture(using: decodable)); 87 | } else { 88 | return AsyncValue.value(decoder.decodeObject(using: decodable)); 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /test/enum/model/color.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | 3 | /// Enums can define static [Codable]s and implement [SelfEncodable] just like normal classes. 4 | enum Color implements SelfEncodable { 5 | none, 6 | green, 7 | blue, 8 | red; 9 | 10 | static const Codable codable = ColorCodable(); 11 | 12 | // This is a more elaborate implementation to showcase the flexibility of the codable protocol. 13 | // You could also just have a fixed string or int encoding for all formats. 14 | @override 15 | void encode(Encoder encoder) { 16 | if (encoder.isHumanReadable()) { 17 | encoder.encodeStringOrNull(switch (this) { 18 | Color.green => 'green', 19 | Color.blue => 'blue', 20 | Color.red => 'red', 21 | Color.none => null, 22 | }); 23 | } else { 24 | encoder.encodeIntOrNull(switch (this) { 25 | Color.green => 0, 26 | Color.blue => 1, 27 | Color.red => 2, 28 | Color.none => null, 29 | }); 30 | } 31 | } 32 | } 33 | 34 | class ColorCodable extends SelfCodable { 35 | const ColorCodable(); 36 | 37 | // This is a more elaborate implementation to showcase the flexibility of the codable protocol. 38 | // You could also just have a fixed string or int decoding for all formats. 39 | @override 40 | Color decode(Decoder decoder) { 41 | return switch (decoder.whatsNext()) { 42 | // Enums (as any other class) may treat 'null' as a value or fallback to a default value. 43 | DecodingType.nil => Color.none, 44 | DecodingType.string => decodeString(decoder.decodeStringOrNull(), decoder), 45 | DecodingType.num || DecodingType.int => decodeInt(decoder.decodeIntOrNull(), decoder), 46 | DecodingType.unknown when decoder.isHumanReadable() => decodeString(decoder.decodeStringOrNull(), decoder), 47 | DecodingType.unknown => decodeInt(decoder.decodeIntOrNull(), decoder), 48 | _ => decoder.expect('Color as string or int'), 49 | }; 50 | } 51 | 52 | Color decodeString(String? value, Decoder decoder) { 53 | return switch (value) { 54 | 'green' => Color.green, 55 | 'blue' => Color.blue, 56 | 'red' => Color.red, 57 | // Enums (as any other class) may treat 'null' as a value or fallback to a default value. 58 | null => Color.none, 59 | // Throw an error on any unknown value. We could also choose a default value here as well. 60 | _ => decoder.expect('Color of green, blue, red or null'), 61 | }; 62 | } 63 | 64 | Color decodeInt(int? value, Decoder decoder) { 65 | return switch (value) { 66 | 0 => Color.green, 67 | 1 => Color.blue, 68 | 2 => Color.red, 69 | // Enums (as any other class) may treat 'null' as a value or fallback to a default value. 70 | null => Color.none, 71 | // Throw an error on any unknown value. We could also choose a default value here as well. 72 | _ => decoder.expect('Color of 0, 1, 2 or 3'), 73 | }; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /codable_builder/test/with_analyzer/codable_class_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_builder/codable_builder.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import '../utils.dart'; 5 | 6 | void main() { 7 | group('codable class (analyzer)', () { 8 | test('should build normal class', () async { 9 | final element = await getElementFrom('basic_class.dart'); 10 | 11 | final node = CodableClassNode.fromElement(element); 12 | final code = builder.buildCodableClass(node); 13 | 14 | expect( 15 | emit(code), 16 | equals('class TestClassCodable extends Codable {\n' 17 | ' const TestClassCodable();\n' 18 | '\n' 19 | ' @override\n' 20 | ' TestClass decode(Decoder decoder) {\n' 21 | ' return switch (decoder.whatsNext()) {\n' 22 | ' DecodingType.mapped ||\n' 23 | ' DecodingType.map => _decodeMapped(decoder.decodeMapped()),\n' 24 | ' DecodingType.keyed ||\n' 25 | ' DecodingType.unknown => _decodeKeyed(decoder.decodeKeyed()),\n' 26 | ' _ => decoder.expect(\'mapped or keyed\'),\n' 27 | ' };\n' 28 | ' }\n' 29 | '\n' 30 | ' TestClass _decodeMapped(MappedDecoder mapped) {\n' 31 | ' return TestClass(\n' 32 | ' name: mapped.decodeString(\'name\'),\n' 33 | ' age: mapped.decodeInt(\'age\'),\n' 34 | ' isActive: mapped.decodeBool(\'isactive\'),\n' 35 | ' );\n' 36 | ' }\n' 37 | '\n' 38 | ' TestClass _decodeKeyed(KeyedDecoder keyed) {\n' 39 | ' late String name;\n' 40 | ' late int age;\n' 41 | ' late bool isActive;\n' 42 | ' for (Object? key; (key = keyed.nextKey()) != null;) {\n' 43 | ' switch (key) {\n' 44 | ' case \'name\':\n' 45 | ' name = keyed.decodeString();\n' 46 | ' case \'age\':\n' 47 | ' age = keyed.decodeInt();\n' 48 | ' case \'isactive\':\n' 49 | ' isActive = keyed.decodeBool();\n' 50 | ' default:\n' 51 | ' keyed.skipCurrentValue();\n' 52 | ' }\n' 53 | ' }\n' 54 | '\n' 55 | ' return TestClass(name: name, age: age, isActive: isActive);\n' 56 | ' }\n' 57 | '\n' 58 | ' @override\n' 59 | ' void encode(TestClass value, Encoder encoder) {\n' 60 | ' final keyed = encoder.encodeKeyed();\n' 61 | ' keyed.encodeString(\'name\', value.name);\n' 62 | ' keyed.encodeInt(\'age\', value.age);\n' 63 | ' keyed.encodeBool(\'isactive\', value.isActive);\n' 64 | ' keyed.end();\n' 65 | ' }\n' 66 | '}\n' 67 | ''), 68 | ); 69 | }); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /codable_builder/lib/src/models/nodes.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/element/element2.dart'; 2 | 3 | import '../analyzer/nodes.dart'; 4 | import 'types.dart'; 5 | 6 | class CodableClassNode { 7 | CodableClassNode({ 8 | required this.name, 9 | required this.url, 10 | this.isSelfEncodable = false, 11 | this.constructorName, 12 | this.typeParameters = const [], 13 | required this.fields, 14 | this.subclasses = const [], 15 | }); 16 | 17 | factory CodableClassNode.fromElement(ClassElement2 e) => e.toNode(); 18 | 19 | final String name; 20 | final String url; 21 | final bool isSelfEncodable; 22 | 23 | final String? constructorName; 24 | final List typeParameters; 25 | final List fields; 26 | 27 | final List subclasses; 28 | } 29 | 30 | class CodableFieldNode { 31 | CodableFieldNode({ 32 | required this.name, 33 | required this.key, 34 | required this.type, 35 | this.id, 36 | this.isNamed = false, 37 | }); 38 | 39 | final String name; 40 | final String key; 41 | final CodableType type; 42 | final int? id; 43 | final bool isNamed; 44 | } 45 | 46 | class CodableTypeParameter { 47 | CodableTypeParameter({ 48 | required this.name, 49 | this.bound, 50 | }); 51 | 52 | final String name; 53 | final CodableType? bound; 54 | } 55 | 56 | class CodableSubclassNode { 57 | CodableSubclassNode({ 58 | required this.name, 59 | required this.url, 60 | this.chainable = false, 61 | this.typeParameterMapping = const [], 62 | }); 63 | 64 | final String name; 65 | final String url; 66 | final bool chainable; 67 | final List typeParameterMapping; 68 | } 69 | 70 | class CodableTypeParameterMapping { 71 | CodableTypeParameterMapping({ 72 | required this.name, 73 | this.bound, 74 | this.derivedIndex = -1, 75 | this.compoundIndex = -1, 76 | }); 77 | 78 | final String name; 79 | final CodableType? bound; 80 | 81 | // What index of the superclass type param this maps to, or -1 if none. 82 | final int derivedIndex; 83 | // What index of the compound type params this maps to, or -1 if none. 84 | final int compoundIndex; 85 | } 86 | 87 | class CodableEnumNode { 88 | CodableEnumNode({ 89 | required this.name, 90 | required this.url, 91 | this.isSelfEncodable = false, 92 | required this.values, 93 | this.nullValue, 94 | this.fallbackValue, 95 | }); 96 | 97 | final String name; 98 | final String url; 99 | final bool isSelfEncodable; 100 | final List values; 101 | final String? nullValue; 102 | final String? fallbackValue; 103 | } 104 | 105 | class CodableEnumValue { 106 | CodableEnumValue({ 107 | required this.name, 108 | required this.stringValue, 109 | required this.intValue, 110 | }); 111 | 112 | final String name; 113 | final String? stringValue; 114 | final int intValue; 115 | } 116 | -------------------------------------------------------------------------------- /test/hooks/delegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | 3 | abstract class RecursiveDelegatingDecoder implements Decoder { 4 | RecursiveDelegatingDecoder(this.delegate); 5 | final Decoder delegate; 6 | 7 | RecursiveDelegatingDecoder wrap(Decoder decoder); 8 | 9 | @override 10 | DecodingType whatsNext() => delegate.whatsNext(); 11 | 12 | @override 13 | bool decodeBool() => delegate.decodeBool(); 14 | 15 | @override 16 | bool? decodeBoolOrNull() => delegate.decodeBoolOrNull(); 17 | 18 | @override 19 | int decodeInt() => delegate.decodeInt(); 20 | 21 | @override 22 | int? decodeIntOrNull() => delegate.decodeIntOrNull(); 23 | 24 | @override 25 | double decodeDouble() => delegate.decodeDouble(); 26 | 27 | @override 28 | double? decodeDoubleOrNull() => delegate.decodeDoubleOrNull(); 29 | 30 | @override 31 | num decodeNum() => delegate.decodeNum(); 32 | 33 | @override 34 | num? decodeNumOrNull() => delegate.decodeNumOrNull(); 35 | 36 | @override 37 | String decodeString() => delegate.decodeString(); 38 | 39 | @override 40 | String? decodeStringOrNull() => delegate.decodeStringOrNull(); 41 | 42 | @override 43 | bool decodeIsNull() => delegate.decodeIsNull(); 44 | 45 | @override 46 | T decodeObject({Decodable? using}) => delegate.decodeObject(using: using?.wrap(this)); 47 | 48 | @override 49 | T? decodeObjectOrNull({Decodable? using}) => delegate.decodeObjectOrNull(using: using?.wrap(this)); 50 | 51 | @override 52 | List decodeList({Decodable? using}) => delegate.decodeList(using: using?.wrap(this)); 53 | 54 | @override 55 | List? decodeListOrNull({Decodable? using}) => delegate.decodeListOrNull(using: using?.wrap(this)); 56 | 57 | @override 58 | Map decodeMap({Decodable? keyUsing, Decodable? valueUsing}) => 59 | delegate.decodeMap(keyUsing: keyUsing?.wrap(this), valueUsing: valueUsing?.wrap(this)); 60 | 61 | @override 62 | Map? decodeMapOrNull({Decodable? keyUsing, Decodable? valueUsing}) => 63 | delegate.decodeMapOrNull(keyUsing: keyUsing?.wrap(this), valueUsing: valueUsing?.wrap(this)); 64 | 65 | @override 66 | IteratedDecoder decodeIterated() => delegate.decodeIterated(); 67 | 68 | @override 69 | KeyedDecoder decodeKeyed() => delegate.decodeKeyed(); 70 | 71 | @override 72 | MappedDecoder decodeMapped() => delegate.decodeMapped(); 73 | 74 | @override 75 | bool isHumanReadable() => delegate.isHumanReadable(); 76 | 77 | @override 78 | Never expect(String expect) => delegate.expect(expect); 79 | } 80 | 81 | extension _Wrap on Decodable { 82 | Decodable wrap(RecursiveDelegatingDecoder decoder) => _WrappedDecodable(this, decoder); 83 | } 84 | 85 | class _WrappedDecodable implements Decodable { 86 | _WrappedDecodable(this._decodable, this._parent); 87 | 88 | final Decodable _decodable; 89 | final RecursiveDelegatingDecoder _parent; 90 | 91 | @override 92 | T decode(Decoder decoder) { 93 | return _decodable.decode(_parent.wrap(decoder)); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/src/common/datetime.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | 3 | /// The format to encode and decode a [DateTime]. 4 | enum DateTimeFormat { 5 | auto, 6 | iso8601, 7 | unixMilliseconds, 8 | } 9 | 10 | /// A [DateTime] codable that can encode and decode a date in different formats. 11 | class DateTimeCodable implements Codable { 12 | const DateTimeCodable({ 13 | this.preferredFormat = DateTimeFormat.auto, 14 | this.convertUtc = false, 15 | }); 16 | 17 | /// The preferred format to encode and decode the date. 18 | /// If [DateTimeFormat.auto] is used, the format will be determined based on the decoder / encoder. 19 | /// If [DateTimeFormat.iso8601] is used, the date will be encoded as an ISO8601 string. 20 | /// If [DateTimeFormat.unixMilliseconds] is used, the date will be encoded as a unix milliseconds integer. 21 | /// 22 | /// If the format supports custom de/encoding of [DateTime], this is ignored. 23 | final DateTimeFormat preferredFormat; 24 | 25 | /// Whether to convert the date 26 | /// - from local to UTC before encoding. 27 | /// - from UTC to local after decoding. 28 | final bool convertUtc; 29 | 30 | @override 31 | DateTime decode(Decoder decoder) { 32 | final decodingType = decoder.whatsNext(); 33 | 34 | final value = switch (decodingType) { 35 | DecodingType() => decoder.decodeObject(), 36 | _ => switch (preferredFormat) { 37 | DateTimeFormat.auto => switch (decoder.whatsNext()) { 38 | DecodingType.string => DateTime.parse(decoder.decodeString()), 39 | DecodingType.int || DecodingType.num => DateTime.fromMillisecondsSinceEpoch(decoder.decodeInt()), 40 | DecodingType.unknown when decoder.isHumanReadable() => DateTime.parse(decoder.decodeString()), 41 | DecodingType.unknown => DateTime.fromMillisecondsSinceEpoch(decoder.decodeInt()), 42 | _ => decoder.expect('string, int or custom date'), 43 | }, 44 | DateTimeFormat.iso8601 => DateTime.parse(decoder.decodeString()), 45 | DateTimeFormat.unixMilliseconds => DateTime.fromMillisecondsSinceEpoch(decoder.decodeInt()), 46 | } 47 | }; 48 | if (convertUtc) { 49 | return value.toLocal(); 50 | } 51 | return value; 52 | } 53 | 54 | @override 55 | void encode(DateTime value, Encoder encoder) { 56 | if (convertUtc) { 57 | value = value.toUtc(); 58 | } 59 | if (encoder.canEncodeCustom()) { 60 | encoder.encodeObject(value); 61 | } else { 62 | switch (preferredFormat) { 63 | case DateTimeFormat.auto: 64 | if (encoder.isHumanReadable()) { 65 | encoder.encodeString(value.toIso8601String()); 66 | } else { 67 | encoder.encodeInt(value.millisecondsSinceEpoch); 68 | } 69 | case DateTimeFormat.iso8601: 70 | encoder.encodeString(value.toIso8601String()); 71 | 72 | case DateTimeFormat.unixMilliseconds: 73 | encoder.encodeInt(value.millisecondsSinceEpoch); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /doc/generics.md: -------------------------------------------------------------------------------- 1 | To support generic classes, the respective `Codable` should accept a child (de/en)codable for each of the class'es type parameters. 2 | 3 | For example the generic class `Box` defines a codable like this: 4 | 5 | ```dart 6 | class Box implements SelfEncodable { 7 | Box(this.label, this.data); 8 | 9 | final String label; 10 | final T data; 11 | 12 | static Codable> codable([Codable? codable]) => BoxCodable(codable); 13 | 14 | @override 15 | void encode(Encoder encoder, [Encodable? encodableT]) { 16 | encoder.encodeKeyed() 17 | ..encodeString('label', label) 18 | ..encodeObject('data', data, using: encodableT) 19 | ..end(); 20 | } 21 | } 22 | 23 | class BoxCodable extends Codable> { 24 | const BoxCodable([Codable? this.codableT]); 25 | 26 | final Codable? codableT; 27 | 28 | @override 29 | void encode(Box value, Encoder encoder) { 30 | value.encode(encoder, codableT); 31 | } 32 | 33 | @override 34 | Box decode(Decoder decoder) { 35 | // For simplicity, we don't check the decoder.whatsNext() here. Don't do this for real implementations. 36 | final mapped = decoder.decodeMapped(); 37 | return Box( 38 | mapped.decodeString('label'), 39 | mapped.decodeObject('data', using: codableT), 40 | ); 41 | } 42 | } 43 | ``` 44 | 45 | As you can see, `encode()` method and `BoxCodable` class accept additional `encodableT` and `ccodableT` parameters for properties of type `T`. Also, the static `codable` member on `Box` is now a method accepting a child codable as well. 46 | 47 | For decoding and encoding a specific `Box`, simply provide a `Codable` for that inner type, e.g.: 48 | 49 | ```dart 50 | final Box box = Box.codable(UriCodable()).fromJson(...); 51 | final String json = Box.codable(UriCodable()).toJson(box); 52 | ``` 53 | 54 | To be able to chain `.toJson()` on `box` directly, we need to define an additional extension: 55 | 56 | ```dart 57 | extension BoxEncodableExtension on Box { 58 | SelfEncodable use([Encodable? encodableT]) { 59 | return SelfEncodable.fromHandler((e) => encode(e, encodableT)); 60 | } 61 | } 62 | ``` 63 | 64 | This allows us to also encode a box like this: 65 | 66 | ```dart 67 | final String json = box.use(UriCodable()).toJson(); 68 | ``` 69 | 70 | ### Reusing Codables 71 | 72 | The 'inner codable' system lets us provide explicit de/encodables for inner types. 73 | 74 | Another benefit of this system is that we can construct non-generic instances that can be passed around freely. 75 | 76 | ```dart 77 | void main() { 78 | final Box box = ...; 79 | 80 | // Constructs a new codable that explicitly handles the [Box] type. 81 | // From now on, there is no conceptual difference to non-generic [Codable]s. 82 | final Codable> boxUriCodable = Box.codable(UriCodable()); 83 | 84 | doSomething>(box, boxUriCodable); 85 | } 86 | 87 | // Accepts a value and codable of the same type. 88 | void doSomething(T value, Codable codable) { 89 | // Here we don't know (or care) if the codable was originally a generic codable. 90 | } 91 | ``` 92 | -------------------------------------------------------------------------------- /test/csv/csv_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:codable_dart/common.dart'; 4 | import 'package:codable_dart/csv.dart'; 5 | import 'package:codable_dart/json.dart'; 6 | import 'package:codable_dart/standard.dart'; 7 | import 'package:test/test.dart'; 8 | 9 | import 'model/measures.dart'; 10 | import 'test_data.dart'; 11 | 12 | void main() { 13 | group('csv', () { 14 | // Since CSV always deals with rows of data, the fromCsv and toCsv methods deal directly 15 | // with Lists of objects instead of single objects. 16 | 17 | test('decodes', () { 18 | // Use the fromCsv extension method to decode the data. 19 | List measures = Measures.codable.fromCsv(measuresCsv); 20 | expect(measures, equals(measuresObjects)); 21 | }); 22 | 23 | test('encodes', () { 24 | // Use the toCsv extension method to encode the data. 25 | final encoded = measuresObjects.encode.toCsv(); 26 | expect(encoded, equals(measuresCsv)); 27 | }); 28 | 29 | // This shows how to easily switch between data formats given the same model implementation. 30 | test('interop with json', () { 31 | // Use the fromCsv extension method to decode the data from csv. 32 | List measures = Measures.codable.fromCsv(measuresCsv); 33 | // Use the encode.toJson extension method to encode the data to json. 34 | final json = measures.encode.toJson(); 35 | 36 | expect(json, equals(measuresJson)); 37 | 38 | // Use the fromJson extension method to decode the data from json. 39 | List measures2 = Measures.codable.list().fromJson(json); 40 | // Use the toCsv extension method to encode the data to csv. 41 | final csv = measures2.encode.toCsv(); 42 | 43 | expect(csv, equals(measuresCsv)); 44 | }); 45 | 46 | group('codec', () { 47 | test('decodes string', () { 48 | // Use the fromCsv extension method to decode the data. 49 | List measures = Measures.codable.list().codec.fuse(csv).decode(measuresCsv); 50 | expect(measures, equals(measuresObjects)); 51 | }); 52 | 53 | test('encodes string', () { 54 | // Use the toCsv extension method to encode the data. 55 | final encoded = Measures.codable.list().codec.fuse(csv).encode(measuresObjects); 56 | expect(encoded, equals(measuresCsv)); 57 | }); 58 | 59 | test('special cases fusing with utf8', () { 60 | expect(Measures.codable.list().codec.fuse(csv).fuse(utf8).runtimeType.toString(), 61 | contains('_CodableCodec, List>')); 62 | }); 63 | 64 | test('decodes bytes fused', () { 65 | // Use the csvCodec extension fused with the utf8 codec to decode bytes. 66 | List measures = Measures.codable.list().codec.fuse(csv).fuse(utf8).decode(measuresCsvBytes); 67 | expect(measures, equals(measuresObjects)); 68 | }); 69 | 70 | test('encodes bytes fused', () { 71 | // Use the csvCodec extension fused with the utf8 codec to encode bytes. 72 | final encoded = Measures.codable.list().codec.fuse(csv).fuse(utf8).encode(measuresObjects); 73 | expect(encoded, equals(measuresCsvBytes)); 74 | }); 75 | }); 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/common/map.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | import 'package:codable_dart/extended.dart'; 3 | 4 | import '../formats/standard.dart'; 5 | 6 | extension AsMapCodable on Codable { 7 | /// Returns a [Codable] that can encode and decode a map of [K] and [T]. 8 | /// 9 | /// This let's you use any format extensions with maps: 10 | /// ```dart 11 | /// final Map people = Person.codable.map().fromJson(...); 12 | /// final String json = Person.codable.map().toJson(people); 13 | /// ``` 14 | /// 15 | /// Optionally you can provide a [keyCodable] to specify how to encode and decode the keys. 16 | /// ```dart 17 | /// final Map people = Person.codable.map(Uri.codable).fromJson(...); 18 | /// final String json = Person.codable.map(Uri.codable).toJson(people); 19 | /// ``` 20 | Codable> map([Codable? keyCodable]) => MapCodable(keyCodable, this); 21 | } 22 | 23 | extension AsMapEncodable on Map { 24 | /// Returns an [Encodable] that can encode a map of [K] and [T]. 25 | /// 26 | /// This let's you use any format extensions directly on a [Map] of [Encodable]s: 27 | /// ```dart 28 | /// final Map people = ...; 29 | /// final String json = people.encode.toJson(); 30 | /// ``` 31 | SelfEncodable get encode => MapSelfEncodable(this); 32 | } 33 | 34 | /// A [Codable] that can encode and decode a map of [K] and [V]. 35 | /// 36 | /// Prefer using [AsMapCodable.map] instead of the constructor. 37 | class MapCodable implements Codable>, ComposedDecodable2> { 38 | const MapCodable(this.keyCodable, this.codable); 39 | 40 | final Codable? keyCodable; 41 | final Codable codable; 42 | 43 | @override 44 | Map decode(Decoder decoder) { 45 | return switch (decoder.whatsNext()) { 46 | DecodingType.map || 47 | DecodingType.mapped || 48 | DecodingType.unknown => 49 | decoder.decodeMap(keyUsing: keyCodable, valueUsing: codable), 50 | DecodingType.keyed => decodeKeyed(decoder.decodeKeyed()), 51 | _ => decoder.expect('map or keyed'), 52 | }; 53 | } 54 | 55 | Map decodeKeyed(KeyedDecoder keyed) { 56 | final map = {}; 57 | for (Object? key; (key = keyed.nextKey()) != null;) { 58 | if (keyCodable != null && key is! K) { 59 | key = StandardDecoder.decode(key, using: keyCodable!); 60 | } 61 | map[key as K] = keyed.decodeObject(using: codable); 62 | } 63 | return map; 64 | } 65 | 66 | @override 67 | void encode(Map value, Encoder encoder) { 68 | encoder.encodeMap(value, keyUsing: keyCodable, valueUsing: codable); 69 | } 70 | 71 | @override 72 | R extract(R Function(Codable? codableA, Codable? codableB) fn) { 73 | return fn(keyCodable, codable); 74 | } 75 | } 76 | 77 | /// An [Encodable] that can encode a map of [K] and [T]. 78 | /// 79 | /// Prefer using [AsMapEncodable.encode] instead of the constructor. 80 | class MapSelfEncodable implements SelfEncodable { 81 | const MapSelfEncodable(this.value); 82 | 83 | final Map value; 84 | 85 | @override 86 | void encode(Encoder encoder) { 87 | encoder.encodeMap(value); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /ABOUT.md: -------------------------------------------------------------------------------- 1 | # Codable: Serialization Protocol for Dart 2 | 3 | This package contains the RFC and prototype implementation for a new serialization protocol for Dart. 4 | 5 | 👉 **Read the RFC:** [RFC: New Serialization Protocol for Dart](https://github.com/schultek/codable/blob/main/docs/rfc.md) 6 | 7 | --- 8 | 9 | ### Codebase Overview 10 | 11 | - **Core Protocol:** [/lib/src/core](https://github.com/schultek/codable/tree/main/lib/src/core) 12 | 13 | - **Use cases:** 14 | 15 | - Basic: [/test/basic](https://github.com/schultek/codable/tree/main/test/basic) 16 | - Collections: [/test/collections](https://github.com/schultek/codable/tree/main/test/collections) 17 | - Error Handling: [/test/error_handling](https://github.com/schultek/codable/tree/main/test/error_handling) 18 | - Generics: [/test/generics](https://github.com/schultek/codable/tree/main/test/generics) 19 | - Polymorphism: [/test/polymorphism](https://github.com/schultek/codable/tree/main/test/polymorphism) 20 | 21 | - **Benchmark**: [/test/benchmark](https://github.com/schultek/codable/tree/main/test/benchmark) 22 | 23 | - **Format Implementations:** 24 | 25 | - Standard: [/lib/src/formats/standard](https://github.com/schultek/codable/tree/main/lib/src/formats/standard.dart) 26 | - JSON: [/src/formats/json](https://github.com/schultek/codable/tree/main/lib/src/formats/json.dart) 27 | - MessagePack: [/src/formats/msgpack](https://github.com/schultek/codable/tree/main/lib/src/formats/msgpack.dart) 28 | - CSV: [/src/formats/csv](https://github.com/schultek/codable/tree/main/lib/src/formats/csv.dart) 29 | 30 | - **Type Implementations:** 31 | 32 | - Person: [/test/basic/model/person](https://github.com/schultek/codable/tree/main/test/basic/model/person.dart) 33 | - Color: [/test/enum/model/color](https://github.com/schultek/codable/tree/main/test/enum/model/color.dart) 34 | - List & Set: [/lib/src/common/iterable](https://github.com/schultek/codable/tree/main/lib/src/common/iterable.dart) 35 | - Map: [/lib/src/common/map](https://github.com/schultek/codable/tree/main/lib/src/common/map.dart) 36 | - DateTime: [/lib/src/common/datetime](https://github.com/schultek/codable/tree/main/lib/src/common/datetime.dart) 37 | - Uri: [/lib/src/common/uri](https://github.com/schultek/codable/tree/main/lib/src/common/uri) 38 | 39 | - **Extended Protocol:** [/lib/src/extended](https://github.com/schultek/codable/tree/main/lib/src/extended) 40 | 41 | --- 42 | 43 | ### How to contribute? 44 | 45 | If you would like to contribute, there are several ways to do so. 46 | 47 | First, just **[read the RFC](https://github.com/schultek/codable/blob/main/docs/rfc.md)** and give feedback by commenting on the [issue](https://github.com/schultek/codable/issues/1) or the [forum post](https://forum.itsallwidgets.com/t/rfc-new-serialization-protocol-for-dart/2355). 48 | 49 | A ⭐️ is also very appreciated, and you can help by spreading the word about this proposal. 50 | 51 | Finally, you can contribute code for the following things: 52 | 53 | - **Test Cases**: Have a special case or unique problem you need to solve? Contribute a test case and we can make sure it is supported by the protocol. 54 | - **Formats**: Add a new data format implementation or improve the existing ones. 55 | 56 | > [!IMPORTANT] 57 | > Before contributing, please **open an issue first** so others can see what is being worked, discuss ideas, and combine efforts. 58 | -------------------------------------------------------------------------------- /test/benchmark/performance_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:codable_dart/json.dart'; 5 | import 'package:codable_dart/standard.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | import '../basic/model/person.dart'; 9 | import '../basic/test_data.dart'; 10 | import '../utils.dart'; 11 | import 'bench.dart'; 12 | 13 | final personDeepData = { 14 | ...personTestData, 15 | 'parent': personTestData, 16 | 'friends': List.filled(50, personTestData), 17 | }; 18 | final personBenchData = { 19 | ...personDeepData, 20 | 'friends': List.filled(200, personDeepData), 21 | }; 22 | final personBenchJson = jsonEncode(personBenchData); 23 | final personBenchJsonBytes = utf8.encode(personBenchJson); 24 | 25 | void main() { 26 | Person p = PersonRaw.fromMapRaw(personBenchData); 27 | 28 | group('benchmark', tags: 'benchmark', () { 29 | compare( 30 | 'STANDARD DECODING (Map -> Person)', 31 | self: () => p = Person.codable.fromMap(personBenchData), 32 | other: () => p = PersonRaw.fromMapRaw(personBenchData), 33 | ); 34 | compare( 35 | 'STANDARD ENCODING (Person -> Map)', 36 | self: () => p.toMap(), 37 | other: () => p.toMapRaw(), 38 | ); 39 | 40 | print(''); 41 | 42 | compare( 43 | 'JSON STRING DECODING (String -> Person)', 44 | self: () => p = Person.codable.fromJson(personBenchJson), 45 | other: () => p = PersonRaw.fromJsonRaw(personBenchJson), 46 | ); 47 | compare( 48 | 'JSON STRING ENCODING (Person -> String)', 49 | self: () => p.toJson(), 50 | other: () => p.toJsonRaw(), 51 | ); 52 | 53 | print(''); 54 | 55 | compare( 56 | 'JSON BYTE DECODING (List -> Person)', 57 | self: () => p = Person.codable.fromJsonBytes(personBenchJsonBytes), 58 | other: () => p = PersonRaw.fromJsonBytesRaw(personBenchJsonBytes), 59 | ); 60 | compare( 61 | 'JSON BYTE ENCODING (Person -> List)', 62 | self: () => p.toJsonBytes(), 63 | other: () => p.toJsonBytesRaw(), 64 | ); 65 | }); 66 | 67 | test('lazy benchmark', () async { 68 | final chunkSize = 0xFFFF; 69 | final data = personBenchJsonBytes; 70 | await benchSink('stream', (t) async { 71 | await chunkedAsync(data, t(CallbackSink((value) {})), chunkSize: chunkSize); 72 | }); 73 | 74 | bench('sync', () { 75 | p = Person.codable.fromJsonBytes(data); 76 | }, times: 4); 77 | 78 | bench('single chunk', () { 79 | var sink = Person.codable.codec.fuse(json).fuse(utf8).decoder.startChunkedConversion(CallbackSink((person) { 80 | p = person; 81 | })); 82 | sink.add(data); 83 | }); 84 | 85 | await benchSink('chunked', (t) async { 86 | var sink = Person.codable.codec.fuse(json).fuse(utf8).decoder.startChunkedConversion(CallbackSink((person) { 87 | p = person; 88 | })); 89 | await chunkedAsync(data, t(sink), chunkSize: chunkSize); 90 | }); 91 | 92 | 93 | await benchSink('collected', (t) async { 94 | final builder = BytesBuilder(); 95 | await chunkedAsync( 96 | data, 97 | t(CallbackSink((value) { 98 | builder.add(value); 99 | }, () { 100 | p = Person.codable.fromJsonBytes(builder.takeBytes()); 101 | })), 102 | chunkSize: chunkSize, 103 | ); 104 | }); 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /lib/src/helpers/binary_tokens.dart: -------------------------------------------------------------------------------- 1 | // COPYRIGHT NOTICE: 2 | // This file contains code derived from the 'crimson' package, 3 | // licensed under Apache License 2.0 by Simon Choi. 4 | 5 | const tokenSpace = 0x20; 6 | const tokenLineFeed = 0x0A; 7 | const tokenCarriageReturn = 0x0D; 8 | const tokenTab = 0x09; 9 | const tokenBackspace = 0x08; 10 | const tokenFormFeed = 0x0C; 11 | 12 | const tokenDoubleQuote = 0x22; 13 | const tokenSlash = 0x2F; 14 | const tokenBackslash = 0x5C; 15 | const tokenComma = 0x2C; 16 | const tokenPeriod = 0x2E; 17 | const tokenColon = 0x3A; 18 | const tokenLBracket = 0x5B; 19 | const tokenRBracket = 0x5D; 20 | const tokenLBrace = 0x7B; 21 | const tokenRBrace = 0x7D; 22 | const tokenDollarSign = 0x24; 23 | 24 | const tokenA = 0x61; 25 | const tokenB = 0x62; 26 | const tokenC = 0x63; 27 | const tokenD = 0x64; 28 | const tokenE = 0x65; 29 | const tokenUpperE = 0x45; 30 | const tokenF = 0x66; 31 | const tokenL = 0x6C; 32 | const tokenN = 0x6E; 33 | const tokenR = 0x72; 34 | const tokenS = 0x73; 35 | const tokenT = 0x74; 36 | const tokenU = 0x75; 37 | 38 | const tokenZero = 0x30; 39 | const tokenOne = 0x31; 40 | const tokenTwo = 0x32; 41 | const tokenThree = 0x33; 42 | const tokenFour = 0x34; 43 | const tokenFive = 0x35; 44 | const tokenSix = 0x36; 45 | const tokenSeven = 0x37; 46 | const tokenEight = 0x38; 47 | const tokenNine = 0x39; 48 | const tokenPlus = 0x2B; 49 | const tokenMinus = 0x2D; 50 | 51 | const powersOfTen = [ 52 | 1.0, // 0 53 | 10.0, 54 | 100.0, 55 | 1000.0, 56 | 10000.0, 57 | 100000.0, // 5 58 | 1000000.0, 59 | 10000000.0, 60 | 100000000.0, 61 | 1000000000.0, 62 | 10000000000.0, // 10 63 | 100000000000.0, 64 | 1000000000000.0, 65 | 10000000000000.0, 66 | 100000000000000.0, 67 | 1000000000000000.0, // 15 68 | 10000000000000000.0, 69 | 100000000000000000.0, 70 | 1000000000000000000.0, 71 | 10000000000000000000.0, 72 | 100000000000000000000.0, // 20 73 | 1000000000000000000000.0, 74 | 10000000000000000000000.0, 75 | ]; 76 | 77 | const oneByteLimit = 0x7f; // 7 bits 78 | const twoByteLimit = 0x7ff; // 11 bits 79 | const surrogateTagMask = 0xFC00; 80 | const surrogateValueMask = 0x3FF; 81 | const leadSurrogateMin = 0xD800; 82 | 83 | const maxInt = 9223372036854775807; 84 | 85 | const canDirectWrite = [ 86 | false, false, false, false, false, false, false, false, // 87 | false, false, false, false, false, false, false, false, // 88 | false, false, false, false, false, false, false, false, // 89 | false, false, false, false, false, false, false, false, // 90 | 91 | true, true, false /* " */, true, true, true, true, true, // 92 | true, true, true, true, true, true, true, true, // 93 | true, true, true, true, true, true, true, true, // 94 | true, true, true, true, true, true, true, true, // 95 | 96 | true, true, true, true, true, true, true, true, // 97 | true, true, true, true, true, true, true, true, // 98 | true, true, true, true, true, true, true, true, // 99 | true, true, true, true, false /* \ */, true, true, true, // 100 | 101 | true, true, true, true, true, true, true, true, // 102 | true, true, true, true, true, true, true, true, // 103 | true, true, true, true, true, true, true, true, // 104 | true, true, true, true, true, true, true, true, // 105 | ]; 106 | 107 | const hexDigits = [ 108 | tokenZero, tokenOne, tokenTwo, tokenThree, tokenFour, // 109 | tokenFive, tokenSix, tokenSeven, tokenEight, tokenNine, // 110 | tokenA, tokenB, tokenC, tokenD, tokenE, tokenF, // 111 | ]; 112 | -------------------------------------------------------------------------------- /codable_builder/lib/src/builders/polymorphic_mixin.dart: -------------------------------------------------------------------------------- 1 | part of 'codable_builder.dart'; 2 | 3 | abstract mixin class PolymorphicMixin implements CodableBuilder { 4 | Method _buildDiscriminatorKey(CodableClassNode node) { 5 | return Method((b) { 6 | b.name = 'discriminatorKey'; 7 | b.type = MethodType.getter; 8 | b.returns = refer('String', dartCoreUrl); 9 | b.annotations.add(refer('override', dartCoreUrl)); 10 | b.lambda = true; 11 | b.body = Code('\'type\''); 12 | }); 13 | } 14 | 15 | Method _buildDiscriminators(CodableClassNode node) { 16 | return Method((b) { 17 | b.name = 'discriminators'; 18 | b.type = MethodType.getter; 19 | b.returns = TypeReference((b) { 20 | b.symbol = 'List'; 21 | b.url = dartCoreUrl; 22 | b.types.add(TypeReference((b) { 23 | b.symbol = 'Discriminator'; 24 | b.url = codableExtendedUrl; 25 | b.types.add(refer(node.name, node.url)); 26 | })); 27 | }); 28 | b.annotations.add(refer('override', dartCoreUrl)); 29 | b.lambda = true; 30 | b.body = Code.scope((s) { 31 | final buffer = StringBuffer(); 32 | buffer.writeln('[\n'); 33 | 34 | for (final sub in node.subclasses) { 35 | final ref = refer(sub.name, sub.url); 36 | final val = '\'${sub.name.toLowerCase()}\''; 37 | 38 | if (node.typeParameters.isEmpty) { 39 | buffer.write(' Discriminator<${s(ref)}>($val, ${sub.name}Codable.new),\n'); 40 | } else if (node.typeParameters.length <= 2) { 41 | final bounded = sub.typeParameterMapping.any((m) => m.derivedIndex != -1 && m.bound != null); 42 | 43 | if (!bounded) { 44 | buffer.write( 45 | ' Discriminator.arg${node.typeParameters.length}<${s(ref)}>($val, <${node.typeParameters.map((t) => t.name).join(', ')}>('); 46 | } else { 47 | final bounds = node.typeParameters.indexed.map((v) { 48 | final m = sub.typeParameterMapping.where((m) => m.derivedIndex == v.$1).firstOrNull; 49 | return m != null ? s(m.bound!.reference) : 'dynamic'; 50 | }).toList(); 51 | buffer.write( 52 | ' Discriminator.arg1Bounded<${s(ref)}, ${bounds.join(', ')}>($val, <${node.typeParameters.indexed.map((v) => '${v.$2.name} extends ${bounds[v.$1]}').join(', ')}>('); 53 | } 54 | buffer.write( 55 | '${node.typeParameters.map((t) => '${s(refer('Decodable', codableCoreUrl))}<${t.name}>? decodable${t.name}').join(', ')}) {\n'); 56 | 57 | buffer.write(' return '); 58 | if (sub.typeParameterMapping.isEmpty) { 59 | buffer.write('${sub.name}Codable();\n'); 60 | } else { 61 | buffer.write( 62 | '${sub.name}Codable<${sub.typeParameterMapping.map((m) => m.derivedIndex != -1 ? node.typeParameters[m.derivedIndex].name : 'dynamic').join(', ')}>.of('); 63 | buffer.write(sub.typeParameterMapping.where((m) => m.derivedIndex != -1).map((m) { 64 | return 'decodable${m.name}: decodable${node.typeParameters[m.derivedIndex].name}'; 65 | }).join(', ')); 66 | buffer.write(');\n'); 67 | } 68 | 69 | buffer.write(' }),\n'); 70 | } 71 | 72 | if (sub.chainable) { 73 | // TODO add chainable discriminators 74 | } 75 | } 76 | 77 | buffer.writeln(' ]'); 78 | return buffer.toString(); 79 | }); 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/csv/model/measures.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/common.dart'; 2 | import 'package:codable_dart/core.dart'; 3 | 4 | class Measures implements SelfEncodable { 5 | Measures(this.id, this.name, this.age, this.isActive, this.signupDate, this.website); 6 | 7 | static const Codable codable = MeasuresCodable(); 8 | 9 | final String id; 10 | final String? name; 11 | final int age; 12 | final bool isActive; 13 | final DateTime? signupDate; 14 | final Uri? website; 15 | 16 | @override 17 | bool operator ==(Object other) { 18 | return identical(this, other) || 19 | other is Measures && 20 | runtimeType == other.runtimeType && 21 | id == other.id && 22 | name == other.name && 23 | age == other.age && 24 | isActive == other.isActive && 25 | signupDate == other.signupDate && 26 | website == other.website; 27 | } 28 | 29 | @override 30 | int get hashCode => Object.hash(id, name, age, isActive, signupDate, website); 31 | 32 | @override 33 | String toString() { 34 | return 'Measures{id: $id, name: $name, age: $age, isActive: $isActive, signupDate: $signupDate, website: $website}'; 35 | } 36 | 37 | @override 38 | void encode(Encoder encoder) { 39 | final keyed = encoder.encodeKeyed(); 40 | keyed.encodeString('id', id); 41 | keyed.encodeStringOrNull('name', name); 42 | keyed.encodeInt('age', age); 43 | keyed.encodeBool('isActive', isActive); 44 | keyed.encodeObjectOrNull('signupDate', signupDate, using: const DateTimeCodable()); 45 | keyed.encodeObjectOrNull('website', website, using: const UriCodable()); 46 | keyed.end(); 47 | } 48 | } 49 | 50 | class MeasuresCodable extends SelfCodable { 51 | const MeasuresCodable(); 52 | 53 | @override 54 | Measures decode(Decoder decoder) { 55 | return switch (decoder.whatsNext()) { 56 | DecodingType.mapped || DecodingType.map => decodeMapped(decoder.decodeMapped()), 57 | DecodingType.keyed || DecodingType.unknown => decodeKeyed(decoder.decodeKeyed()), 58 | _ => decoder.expect('keyed or mapped'), 59 | }; 60 | } 61 | 62 | Measures decodeKeyed(KeyedDecoder decoder) { 63 | late String id; 64 | late String? name; 65 | late int age; 66 | late bool isActive; 67 | late DateTime? signupDate; 68 | late Uri? website; 69 | 70 | for (Object? key; (key = decoder.nextKey()) != null;) { 71 | switch (key) { 72 | case 'id': 73 | id = decoder.decodeString(); 74 | case 'name': 75 | name = decoder.decodeStringOrNull(); 76 | case 'age': 77 | age = decoder.decodeInt(); 78 | case 'isActive': 79 | isActive = decoder.decodeBool(); 80 | case 'signupDate': 81 | signupDate = decoder.decodeObjectOrNull(using: const DateTimeCodable()); 82 | case 'website': 83 | website = decoder.decodeObjectOrNull(using: const UriCodable()); 84 | default: 85 | decoder.skipCurrentValue(); 86 | } 87 | } 88 | 89 | return Measures(id, name, age, isActive, signupDate, website); 90 | } 91 | 92 | Measures decodeMapped(MappedDecoder decoder) { 93 | return Measures( 94 | decoder.decodeString('id'), 95 | decoder.decodeStringOrNull('name'), 96 | decoder.decodeInt('age'), 97 | decoder.decodeBool('isActive'), 98 | decoder.decodeObjectOrNull('signupDate', using: const DateTimeCodable()), 99 | decoder.decodeObjectOrNull('website', using: const UriCodable()), 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /codable_builder/lib/src/analyzer/nodes.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/element/element2.dart'; 2 | import 'package:analyzer/dart/element/nullability_suffix.dart'; 3 | import 'package:analyzer/dart/element/type.dart'; 4 | 5 | import '../../codable_builder.dart'; 6 | import '../models/urls.dart'; 7 | 8 | extension ClassElement2Extension on ClassElement2 { 9 | CodableClassNode toNode() { 10 | final isSelfEncodable = allSupertypes.any((t) => 11 | t.element3.name3 == 'SelfEncodable' && 12 | t.element3.library2.uri.toString() == 'package:codable/src/core/interfaces.dart'); 13 | 14 | // TODO: Support choosing a different constructor. 15 | var constructorName = constructors2.firstOrNull?.name3; 16 | if (constructorName == 'new') { 17 | constructorName = null; 18 | } 19 | 20 | return CodableClassNode( 21 | name: name3 ?? '', 22 | url: firstFragment.libraryFragment.source.uri.toString(), 23 | isSelfEncodable: isSelfEncodable, 24 | constructorName: constructorName, 25 | typeParameters: typeParameters2.map((t) => t.toNode()).toList(), 26 | fields: fields2.map((f) => f.toNode()).toList(), 27 | ); 28 | } 29 | } 30 | 31 | extension FieldElement2Extension on FieldElement2 { 32 | CodableFieldNode toNode() { 33 | return CodableFieldNode( 34 | name: name3 ?? '', 35 | key: (name3 ?? '').toLowerCase(), // TODO: Handle case styles and custom keys. 36 | type: type.toCodableType(), 37 | id: null, // TODO: Handle field IDs. 38 | isNamed: true, // TODO: Get this from the constructor. 39 | ); 40 | } 41 | } 42 | 43 | extension TypeParameterElement2Extension on TypeParameterElement2 { 44 | CodableTypeParameter toNode() { 45 | return CodableTypeParameter( 46 | name: name3 ?? '', 47 | bound: bound?.toCodableType(), 48 | ); 49 | } 50 | } 51 | 52 | extension TypeExtension on DartType { 53 | CodableType toCodableType() { 54 | bool isNullable = isDartCoreNull || nullabilitySuffix == NullabilitySuffix.question; 55 | 56 | if (isDartCoreString) { 57 | return isNullable ? CodableType.stringNullable : CodableType.string; 58 | } else if (isDartCoreInt) { 59 | return isNullable ? CodableType.intNullable : CodableType.int; 60 | } else if (isDartCoreDouble) { 61 | return isNullable ? CodableType.doubleNullable : CodableType.double; 62 | } else if (isDartCoreBool) { 63 | return isNullable ? CodableType.boolNullable : CodableType.bool; 64 | } else if (isDartCoreNum) { 65 | return isNullable ? CodableType.numNullable : CodableType.num; 66 | } else if (isDartCoreObject) { 67 | return isNullable ? CodableType.objectNullable : CodableType.object; 68 | } 69 | 70 | if (isDartCoreList) { 71 | return ListCodableType( 72 | (this as InterfaceType).typeArguments.first.toCodableType(), 73 | isNullable, 74 | ); 75 | } 76 | if (isDartCoreMap) { 77 | final typeArgs = (this as InterfaceType).typeArguments; 78 | return MapCodableType( 79 | typeArgs[0].toCodableType(), 80 | typeArgs[1].toCodableType(), 81 | isNullable, 82 | ); 83 | } 84 | 85 | if (this case InterfaceType type) { 86 | final element = type.element3; 87 | return ObjectCodableType( 88 | element.name3 ?? '', 89 | element.library2.uri.toString(), 90 | isNullable, 91 | null, // TODO: Handle 'using' if needed. 92 | ); 93 | } 94 | 95 | return ObjectCodableType( 96 | 'Object', 97 | dartCoreUrl, 98 | isNullable, 99 | null, // TODO: Handle 'using' if needed. 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /test/collections/collections_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/common.dart'; 2 | import 'package:codable_dart/core.dart'; 3 | import 'package:codable_dart/json.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | import '../basic/model/person.dart'; 7 | import '../basic/test_data.dart'; 8 | 9 | void main() { 10 | group("collections", () { 11 | final List personList = [ 12 | for (int i = 0; i < 10; i++) PersonRaw.fromMapRaw({...personTestData, 'name': 'Person $i'}), 13 | ]; 14 | final String personListJson = '[${personList.map((p) => p.toJsonRaw()).join(',')}]'; 15 | 16 | test("decodes as list", () { 17 | // Get the codable for a list of persons. 18 | final Codable> codable = Person.codable.list(); 19 | // Use the fromJson extension method to decode the list. 20 | List list = codable.fromJson(personListJson); 21 | expect(list, equals(personList)); 22 | }); 23 | 24 | test("encodes to list", () { 25 | // Use the encode.toJson extension method to encode the list. 26 | final encoded = personList.encode.toJson(); 27 | expect(encoded, equals(personListJson)); 28 | }); 29 | 30 | final Set personSet = personList.toSet(); 31 | 32 | test("decodes as set", () { 33 | // Get the codable for a set of persons. 34 | final Codable> codable = Person.codable.set(); 35 | // Use the fromJson extension method to decode the set. 36 | Set set = codable.fromJson(personListJson); 37 | expect(set, equals(personSet)); 38 | }); 39 | 40 | test("encodes to set", () { 41 | // Use the encode.toJson extension method to encode the set. 42 | final encoded = personSet.encode.toJson(); 43 | expect(encoded, equals(personListJson)); 44 | }); 45 | 46 | final Map personMap = { 47 | for (final p in personList) p.name: p, 48 | }; 49 | final String personMapJson = '{${personMap.entries.map((e) { 50 | return '"${e.key}":${e.value.toJsonRaw()}'; 51 | }).join(',')}}'; 52 | 53 | test("decodes as map", () { 54 | // Get the codable for a map of strings to persons. 55 | final Codable> codable = Person.codable.map(); 56 | // Use the fromJson extension method to decode the map. 57 | Map map = codable.fromJson(personMapJson); 58 | expect(map, equals(personMap)); 59 | }); 60 | 61 | test("encodes to map", () { 62 | // Use the encode.toJson extension method to encode the map. 63 | final encoded = personMap.encode.toJson(); 64 | expect(encoded, equals(personMapJson)); 65 | }); 66 | 67 | final Map personUriMap = { 68 | for (final p in personList) Uri.parse('example.com/person/${p.name}'): p, 69 | }; 70 | final String personUriMapJson = '{${personUriMap.entries.map((e) { 71 | return '"${e.key}":${e.value.toJsonRaw()}'; 72 | }).join(',')}}'; 73 | 74 | test("decodes as uri map", () { 75 | // Construct the codable for a map of uris to persons. 76 | // Provide the explicit codable for the key type. 77 | final Codable> codable = Person.codable.map(UriCodable()); 78 | // Use the fromJson method to decode the map. 79 | Map map = codable.fromJson(personUriMapJson); 80 | expect(map, equals(personUriMap)); 81 | }); 82 | 83 | test("encodes to uri map", () { 84 | // Construct the codable for a map of uris to persons. 85 | // Provide the explicit codable for the key type. 86 | final Codable> codable = Person.codable.map(UriCodable()); 87 | // Use the toJson method to encode the map. 88 | final encoded = codable.toJson(personUriMap); 89 | expect(encoded, equals(personUriMapJson)); 90 | }); 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /codable_builder/lib/src/builders/codable_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:code_builder/code_builder.dart'; 2 | 3 | import '../../codable_builder.dart'; 4 | import '../models/urls.dart'; 5 | 6 | part 'class_builder_mixin.dart'; 7 | part 'enum_builder_mixin.dart'; 8 | part 'encode_mixin.dart'; 9 | part 'decode_mixin.dart'; 10 | part 'polymorphic_mixin.dart'; 11 | 12 | class CodableBuilderImpl with ClassBuilderMixin, EnumBuilderMixin, EncodeMixin, DecodeMixin, PolymorphicMixin implements CodableBuilder { 13 | const CodableBuilderImpl(); 14 | 15 | @override 16 | Spec buildStaticCodableMember(CodableClassNode node) { 17 | if (node.typeParameters.isEmpty) { 18 | return Field((b) { 19 | b.static = true; 20 | b.modifier = FieldModifier.constant; 21 | b.type = TypeReference((b) { 22 | b.symbol = 'Codable'; 23 | b.url = codableCoreUrl; 24 | b.types.add(refer(node.name, node.url)); 25 | }); 26 | b.name = 'codable'; 27 | b.assignment = Code('${node.name}Codable()'); 28 | }); 29 | } else { 30 | return Method((b) { 31 | b.static = true; 32 | b.returns = TypeReference((b) { 33 | b.symbol = 'Codable'; 34 | b.url = codableCoreUrl; 35 | b.types.add(TypeReference((b) { 36 | b.symbol = node.name; 37 | b.url = node.url; 38 | b.types.addAll(node.typeParameters.map((t) => refer(t.name))); 39 | })); 40 | }); 41 | b.name = 'codable'; 42 | b.types.addAll(node.typeParameters.map((t) => TypeReference((b) { 43 | b.symbol = t.name; 44 | b.bound = t.bound?.reference; 45 | }))); 46 | b.optionalParameters.addAll(node.typeParameters.map((t) { 47 | return Parameter((b) { 48 | b.name = 'codable${t.name}'; 49 | b.type = TypeReference((b) { 50 | b.symbol = 'Codable'; 51 | b.url = codableCoreUrl; 52 | b.types.add(refer(t.name)); 53 | b.isNullable = true; 54 | }); 55 | }); 56 | })); 57 | b.body = Block((b) { 58 | b.statements.add( 59 | Code('return ${node.name}Codable(${node.typeParameters.map((t) => 'codable${t.name}').join(', ')});')); 60 | }); 61 | }); 62 | } 63 | } 64 | 65 | @override 66 | Extension? buildGenericUseExtension(CodableClassNode node) { 67 | if (node.typeParameters.isEmpty) { 68 | return null; 69 | } 70 | 71 | return Extension((b) { 72 | b.name = '${node.name}EncodableExtension'; 73 | b.types.addAll(node.typeParameters.map((t) => TypeReference((b) { 74 | b.symbol = t.name; 75 | b.bound = t.bound?.reference; 76 | }))); 77 | b.on = TypeReference((b) { 78 | b.symbol = node.name; 79 | b.url = node.url; 80 | b.types.addAll(node.typeParameters.map((t) => refer(t.name))); 81 | }); 82 | b.methods.add(Method((b) { 83 | b.returns = TypeReference((b) { 84 | b.symbol = 'SelfEncodable'; 85 | b.url = codableCoreUrl; 86 | }); 87 | b.name = 'use'; 88 | b.optionalParameters.addAll(node.typeParameters.map((t) { 89 | return Parameter((b) { 90 | b.name = 'encodable${t.name}'; 91 | b.type = TypeReference((b) { 92 | b.symbol = 'Encodable'; 93 | b.url = codableCoreUrl; 94 | b.types.add(refer(t.name)); 95 | b.isNullable = true; 96 | }); 97 | }); 98 | })); 99 | b.body = Code( 100 | 'return SelfEncodable.fromHandler((e) => encode(e, ${node.typeParameters.map((t) => 'encodable${t.name}').join(', ')}));'); 101 | })); 102 | }); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /test/progressive_json/model/person.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/common.dart'; 2 | import 'package:codable_dart/core.dart'; 3 | import 'package:codable_dart/extended.dart'; 4 | 5 | import 'async_value.dart'; 6 | 7 | class Person implements SelfEncodable { 8 | Person(this.name, this.age, this.parent, this.friends, this.comments); 9 | 10 | static const Codable codable = PersonCodable(); 11 | 12 | final String name; 13 | final int age; 14 | final AsyncValue parent; 15 | final AsyncValue>> friends; 16 | final Stream? comments; 17 | 18 | // @override 19 | // bool operator ==(Object other) => 20 | // identical(this, other) || 21 | // other is Person && 22 | // runtimeType == other.runtimeType && 23 | // name == other.name && 24 | // age == other.age && 25 | // parent == other.parent && 26 | // friends == other.friends && 27 | // comments == other.comments; 28 | 29 | // @override 30 | // int get hashCode => Object.hash(name, age, parent, friends, comments); 31 | 32 | @override 33 | String toString() { 34 | return 'Person(name: $name, age: $age, parent: $parent, friends: $friends)'; 35 | } 36 | 37 | // ====== Codable Code ====== 38 | // Keep in mind that the below code could also easily be generated by macros or a code generator. 39 | 40 | @override 41 | void encode(Encoder encoder) { 42 | encoder.encodeKeyed() 43 | ..encodeString('name', name) 44 | ..encodeInt('age', age) 45 | ..encodeObject('parent', parent, using: personNullAsync) 46 | ..encodeObject('friends', friends, using: personListAsync) 47 | ..encodeStreamOrNull('comments', comments) 48 | ..end(); 49 | } 50 | } 51 | 52 | final personNullAsync = Person.codable.orNull.async(); 53 | final personListAsync = Person.codable.async().list().async(); 54 | 55 | /// Codable implementation for [Person]. 56 | /// 57 | /// This extends the [SelfCodable] class for a default implementation of [encode] and 58 | /// implements the [decode] method. 59 | class PersonCodable extends SelfCodable { 60 | const PersonCodable(); 61 | 62 | @override 63 | Person decode(Decoder decoder) { 64 | return switch (decoder.whatsNext()) { 65 | // If the format prefers mapped decoding, use mapped decoding. 66 | DecodingType.mapped || DecodingType.map => decodeMapped(decoder.decodeMapped()), 67 | // If the format prefers keyed decoding or is non-self describing, use keyed decoding. 68 | DecodingType.keyed || DecodingType.unknown => decodeKeyed(decoder.decodeKeyed()), 69 | _ => decoder.expect('mapped or keyed'), 70 | }; 71 | } 72 | 73 | Person decodeKeyed(KeyedDecoder keyed) { 74 | late String name; 75 | late int age; 76 | late AsyncValue parent; 77 | late AsyncValue>> friends; 78 | Stream? comments; 79 | 80 | for (Object? key; (key = keyed.nextKey()) != null;) { 81 | switch (key) { 82 | case 'name': 83 | name = keyed.decodeString(); 84 | case 'age': 85 | age = keyed.decodeInt(); 86 | case 'parent': 87 | parent = keyed.decodeObject(using: personNullAsync); 88 | case 'friends': 89 | friends = keyed.decodeObject(using: personListAsync); 90 | case 'comments': 91 | comments = keyed.decodeStreamOrNull(); 92 | default: 93 | keyed.skipCurrentValue(); 94 | } 95 | } 96 | 97 | return Person(name, age, parent, friends, comments); 98 | } 99 | 100 | Person decodeMapped(MappedDecoder mapped) { 101 | return Person( 102 | mapped.decodeString('name'), 103 | mapped.decodeInt('age'), 104 | mapped.decodeObject('parent', using: personNullAsync), 105 | mapped.decodeObject('friends', using: personListAsync), 106 | mapped.decodeStreamOrNull('comments'), 107 | ); 108 | } 109 | } -------------------------------------------------------------------------------- /test/hooks/complex/model/box.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | import 'package:codable_dart/extended.dart'; 3 | 4 | import '../../proxy_hook.dart'; 5 | 6 | abstract class Box implements SelfEncodable { 7 | const Box(this.content); 8 | 9 | final T content; 10 | 11 | static Codable> codable([Codable? codable]) => BoxCodable(codable); 12 | 13 | @override 14 | void encode(Encoder encoder, [Encodable? encodableT]) { 15 | encoder.encodeKeyed() 16 | ..encodeObject('content', content, using: encodableT) 17 | ..end(); 18 | } 19 | } 20 | 21 | extension BoxEncodableExtension on Box { 22 | SelfEncodable use([Encodable? encodableT]) { 23 | return SelfEncodable.fromHandler((e) => encode(e, encodableT)); 24 | } 25 | } 26 | 27 | class NumberBox extends Box { 28 | const NumberBox(super.content); 29 | 30 | @override 31 | void encode(Encoder encoder, [_]) { 32 | encoder.encodeKeyed() 33 | ..encodeString('type', 'number') 34 | ..encodeNum('content', content) 35 | ..end(); 36 | } 37 | } 38 | 39 | class BoxHook with ProxyHook { 40 | const BoxHook(); 41 | } 42 | 43 | class BoxCodable extends CodableWithHooks> { 44 | BoxCodable([Codable? codable]) : super(_BoxCodable(codable), const BoxHook()); 45 | 46 | BoxCodable.of({Encodable? encodable, Decodable? decodable}) 47 | : super(_BoxCodable.of(encodable: encodable, decodable: decodable), const BoxHook()); 48 | } 49 | 50 | class _BoxCodable with SuperDecodable> implements Codable>, ComposedDecodable1> { 51 | const _BoxCodable([Codable? codable]) 52 | : encodable = codable, 53 | decodable = codable; 54 | 55 | const _BoxCodable.of({this.encodable, this.decodable}); 56 | 57 | final Encodable? encodable; 58 | final Decodable? decodable; 59 | 60 | @override 61 | String get discriminatorKey => 'type'; 62 | 63 | @override 64 | List> get discriminators => [ 65 | Discriminator.arg1Bounded('number', (Decodable? decodableT) { 66 | return NumberBoxCodable.of(decodable: decodableT); 67 | }), 68 | ]; 69 | 70 | @override 71 | Decodable> resolveDiscriminator(Discriminator discriminator) { 72 | return discriminator.resolve1, T>(decodable); 73 | } 74 | 75 | @override 76 | void encode(Box value, Encoder encoder) { 77 | value.encode(encoder, encodable); 78 | } 79 | 80 | @override 81 | R extract(R Function(Decodable? decodableA) fn) { 82 | return fn(decodable); 83 | } 84 | } 85 | 86 | class NumberBoxCodable extends CodableWithHooks> { 87 | NumberBoxCodable([Codable? codable]) : super(_NumberBoxCodable(codable), const BoxHook()); 88 | 89 | NumberBoxCodable.of({Encodable? encodable, Decodable? decodable}) 90 | : super(_NumberBoxCodable.of(encodable: encodable, decodable: decodable), const BoxHook()); 91 | 92 | NumberBoxCodable.inherited([Codable? codable]) : super.inherited(_NumberBoxCodable(codable), const BoxHook()); 93 | 94 | NumberBoxCodable.inheritedOf({Encodable? encodable, Decodable? decodable}) 95 | : super.inherited(_NumberBoxCodable.of(encodable: encodable, decodable: decodable), const BoxHook()); 96 | } 97 | 98 | class _NumberBoxCodable implements Codable> { 99 | const _NumberBoxCodable([Codable? codable]) 100 | : encodable = codable, 101 | decodable = codable; 102 | 103 | const _NumberBoxCodable.of({this.encodable, this.decodable}); 104 | 105 | final Encodable? encodable; 106 | final Decodable? decodable; 107 | 108 | @override 109 | NumberBox decode(Decoder decoder) { 110 | final mapped = decoder.decodeMapped(); 111 | return NumberBox( 112 | mapped.decodeObject('content', using: decodable), 113 | ); 114 | } 115 | 116 | @override 117 | void encode(NumberBox value, Encoder encoder) { 118 | value.encode(encoder, encodable); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /test/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:math'; 3 | import 'dart:typed_data'; 4 | 5 | import 'package:codable_dart/src/helpers/binary_tokens.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void chunked(List bytes, Sink> sink, {int chunkSize = 0xFF}) { 9 | int count = (bytes.length / chunkSize).ceil(); 10 | 11 | final Uint8List buffer = bytes is Uint8List ? bytes : Uint8List.fromList(bytes); 12 | 13 | for (var i = 0; i < count; i++) { 14 | final offset = i * chunkSize; 15 | sink.add(Uint8List.view(buffer.buffer, buffer.offsetInBytes + offset, min(chunkSize, buffer.length - offset))); 16 | } 17 | 18 | sink.close(); 19 | } 20 | 21 | Future chunkedAsync(List bytes, Sink> sink, 22 | {int chunkSize = 0xFFFF, Duration delay = const Duration(microseconds: 2000)}) async { 23 | int count = (bytes.length / chunkSize).ceil(); 24 | 25 | final Uint8List buffer = bytes is Uint8List ? bytes : Uint8List.fromList(bytes); 26 | final List chunks = []; 27 | 28 | for (var i = 0; i < count; i++) { 29 | final offset = i * chunkSize; 30 | final chunk = Uint8List.view(buffer.buffer, buffer.offsetInBytes + offset, min(chunkSize, buffer.length - offset)); 31 | chunks.add(chunk); 32 | } 33 | 34 | final completer = Completer(); 35 | 36 | int i = 0; 37 | Timer.periodic(delay, (t) { 38 | sink.add(chunks[i]); 39 | i++; 40 | if (i >= count) { 41 | t.cancel(); 42 | sink.close(); 43 | completer.complete(); 44 | } 45 | }); 46 | 47 | await completer.future; 48 | } 49 | 50 | class CallbackSink implements Sink { 51 | CallbackSink(this._add, [this._done]); 52 | final void Function(T) _add; 53 | final void Function()? _done; 54 | 55 | @override 56 | void add(T data) { 57 | _add(data); 58 | } 59 | 60 | @override 61 | void close() { 62 | _done?.call(); 63 | } 64 | } 65 | 66 | Stream> streamData(List data, {int? splitEveryN, bool Function(int)? split}) async* { 67 | split ??= _makeSplit(splitEveryN); 68 | var chunk = []; 69 | for (final char in data) { 70 | chunk.add(char); 71 | if (split(char)) { 72 | // print("--- Sending chunk: ${utf8.decode(chunk).trim()}"); 73 | yield chunk; 74 | chunk = []; 75 | await Future.delayed(const Duration(milliseconds: 100)); 76 | } 77 | } 78 | if (chunk.isNotEmpty) { 79 | // print("--- Sending final chunk: ${utf8.decode(chunk).trim()}"); 80 | yield chunk; 81 | } 82 | } 83 | 84 | bool Function(int) _makeSplit(int? splitEveryN) { 85 | if (splitEveryN == null || splitEveryN <= 0) { 86 | return (int char) => char == tokenLineFeed; 87 | } 88 | int count = 0; 89 | return (int char) { 90 | count++; 91 | if (count >= splitEveryN) { 92 | count = 0; 93 | return true; 94 | } 95 | return false; 96 | }; 97 | } 98 | 99 | class AsyncTester { 100 | final _controller = StreamController<(int, Object?)>(); 101 | 102 | final List _locks = []; 103 | 104 | void addStream(int tag, Stream s) { 105 | final lock = s.listen((value) { 106 | _controller.add((tag, value)); 107 | }, onError: (error) { 108 | _controller.addError(error); 109 | }).asFuture(); 110 | _addLock(lock); 111 | } 112 | 113 | void addFuture(int tag, Future f) { 114 | _addLock(f.then((value) { 115 | _controller.add((tag, value)); 116 | }).catchError((error) { 117 | _controller.addError(error); 118 | })); 119 | } 120 | 121 | void _addLock(Future lock) { 122 | if (_controller.isClosed) { 123 | throw StateError('Cannot add to a closed AsyncTester.'); 124 | } 125 | _locks.add(lock); 126 | lock.whenComplete(() { 127 | _locks.remove(lock); 128 | if (_locks.isEmpty) { 129 | _controller.close(); 130 | } 131 | }); 132 | } 133 | 134 | Future match(List values) async { 135 | int n = 0; 136 | 137 | (int, Object?) current = (-1, Object()); 138 | await for (final actual in _controller.stream) { 139 | if (identical(actual.$1, current.$1) && identical(actual.$2, current.$2)) { 140 | continue; // Skip duplicates 141 | } 142 | 143 | if (n >= values.length) { 144 | expect(n, values.length, reason: 'More values received than expected.'); 145 | } 146 | 147 | var expected = values[n]; 148 | 149 | if (expected is Function(Object?)) { 150 | expected(actual.$2); 151 | } else if (expected is Function(int, Object?)) { 152 | expected(actual.$1, actual.$2); 153 | } else if (expected is (int, Object?)) { 154 | expect(actual, expected); 155 | } else { 156 | expect(actual.$2, expected); 157 | } 158 | n++; 159 | current = actual; 160 | } 161 | 162 | if (n < values.length) { 163 | expect(n, values.length, reason: 'Less values received than expected.'); 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /lib/src/core/errors.dart: -------------------------------------------------------------------------------- 1 | // A collection of errors that can be thrown by implementations using the codable package. 2 | abstract class CodableException implements Exception { 3 | 4 | String get message; 5 | 6 | @override 7 | String toString() => 'CodableException: $message'; 8 | 9 | factory CodableException.wrap(Object error, {required String method, required String hint}) { 10 | if (error is WrappedCodableException) { 11 | return WrappedCodableException(method, '$hint->${error.hint}', error.error); 12 | } else { 13 | return WrappedCodableException(method, hint, error); 14 | } 15 | } 16 | 17 | /// Throws an [UnsupportedError] with the message "Unsupported method: 'Class.method()'". 18 | /// The message will include a reason if provided. 19 | /// 20 | /// It should primarily be used by a [Decoder] implementation when a decoding method is called that is not supported. 21 | /// For example, when calling [decodeList] on [CsvDecoder], the error would read 22 | /// "Unsupported operation: 'CsvDecoder.decodeList()'. The csv format does not support nested lists.". 23 | /// 24 | /// The [clazz] parameter is the class name. 25 | /// The [method] parameter is the method name. 26 | /// The [reason] parameter is an optional reason for why the method is not supported. 27 | factory CodableException.unsupportedMethod(String clazz, String method, {Object? reason}) { 28 | var message = "'$clazz.$method()'"; 29 | if (reason != null) { 30 | message += '. $reason'; 31 | } 32 | 33 | return CodableUnsupportedError(message); 34 | } 35 | 36 | /// Throws a [FormatException] with the message 'Unexpected type: Expected x but got y "..." at offset z.'. 37 | /// The message will include the expected type, the actual type if provided and the actual token at the provided offset if available. 38 | /// 39 | /// It should be used by a [Decoder] implementation when the [Decoder.expect] method is called, or when an unexpected 40 | /// token in the encoded data is encountered. For example, when a [Decodable] implementation calls [Decoder.decodeString] 41 | /// but the next token is not a string, the error would read 'Unexpected type: Expected string but got number "42" at offset 123.'. 42 | /// 43 | /// The [expected] parameter is the expected type. 44 | /// The [actual] parameter is the actual type if available. 45 | /// The [data] parameter is the encoded data. Supported types are String and List. 46 | /// The [offset] parameter is the offset in the encoded data where the unexpected token was found. 47 | factory CodableException.unexpectedType({required String expected, String? actual, Object? data, int? offset}) { 48 | var message = 'Unexpected type: Expected $expected'; 49 | var actualToken = _tokenAt(data, offset); 50 | if (actual != null) { 51 | message += ' but got $actual'; 52 | if (actualToken != null) { 53 | message += ' $actualToken'; 54 | } 55 | } else if (actualToken != null) { 56 | message += ' but got $actualToken'; 57 | } 58 | if (offset != null) { 59 | message += 'at offset $offset'; 60 | } 61 | message += '.'; 62 | 63 | return CodableFormatException(message, data, offset); 64 | } 65 | 66 | /// Returns a substring of the source data starting at the given offset and of length 5. 67 | /// 68 | /// The [data] parameter must be a String or a List. 69 | static String? _tokenAt(Object? data, int? offset) { 70 | String? token; 71 | if (offset != null) { 72 | if (data is String) { 73 | offset = offset.clamp(0, data.length); 74 | token = data.substring(offset, offset + 5); 75 | } else if (data is List) { 76 | offset = offset.clamp(0, data.length); 77 | token = String.fromCharCodes(data, offset, offset + 5); 78 | } 79 | } 80 | if (token != null) { 81 | return '"${token.replaceAll(r'\', r'\\').replaceAll('"', r'\"')}"'; 82 | } 83 | return null; 84 | } 85 | } 86 | 87 | class CodableFormatException extends FormatException implements CodableException { 88 | CodableFormatException(super.message, super.source, super.offset); 89 | 90 | @override 91 | String toString() => 'CodableException: ${super.toString().substring('FormatException: '.length)}'; 92 | } 93 | 94 | class CodableUnsupportedError extends UnsupportedError implements CodableException { 95 | CodableUnsupportedError(super.message); 96 | 97 | @override 98 | String get message => 'Unsupported method ${super.message!}'; 99 | 100 | @override 101 | String toString() => 'CodableException: $message'; 102 | } 103 | 104 | class WrappedCodableException implements CodableException { 105 | WrappedCodableException(this.method, this.hint, this.error); 106 | 107 | final String method; 108 | final String hint; 109 | final Object error; 110 | 111 | @override 112 | String get message => 113 | 'Failed to $method $hint: ${error is CodableException ? (error as CodableException).message : error}'; 114 | 115 | @override 116 | String toString() { 117 | return 'CodableException: $message'; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /codable_builder/lib/src/builders/enum_builder_mixin.dart: -------------------------------------------------------------------------------- 1 | part of 'codable_builder.dart'; 2 | 3 | abstract mixin class EnumBuilderMixin implements CodableBuilder { 4 | @override 5 | Class buildCodableEnumClass(CodableEnumNode node) { 6 | return Class((b) { 7 | b.name = '${node.name}Codable'; 8 | b.extend = TypeReference((b) { 9 | b.symbol = node.isSelfEncodable ? 'SelfCodable' : 'Codable'; 10 | b.url = codableCoreUrl; 11 | b.types.add(refer(node.name, node.url)); 12 | }); 13 | b.constructors.add(Constructor((b) { 14 | b.constant = true; 15 | })); 16 | 17 | b.methods.add(Method((b) { 18 | b.name = 'decode'; 19 | b.returns = refer(node.name, node.url); 20 | b.annotations.add(refer('override', dartCoreUrl)); 21 | b.requiredParameters.add(Parameter((b) { 22 | b.name = 'decoder'; 23 | b.type = refer('Decoder', codableCoreUrl); 24 | })); 25 | b.body = Block((b) { 26 | b.statements.add(Code.scope((a) => 'return switch (decoder.whatsNext()) {\n' 27 | '${node.nullValue != null ? ' DecodingType.nil => ${a(refer(node.name, node.url))}.${node.nullValue},\n' : ''}' 28 | ' DecodingType.string => _decodeString(decoder.decodeStringOrNull(), decoder),\n' 29 | ' DecodingType.num || DecodingType.int => _decodeInt(decoder.decodeIntOrNull(), decoder),\n' 30 | ' DecodingType.unknown when decoder.isHumanReadable() => _decodeString(decoder.decodeStringOrNull(), decoder),\n' 31 | ' DecodingType.unknown => _decodeInt(decoder.decodeIntOrNull(), decoder),\n' 32 | ' _ => decoder.expect(\'${node.name} as String or int\'),\n' 33 | '};')); 34 | }); 35 | })); 36 | 37 | Block buildDecodeBody(List<(String, String)> cases) { 38 | return Block((b) { 39 | b.statements.add(Code.scope((a) { 40 | String buildCase(String value, String name) { 41 | return ' $value => ${a(refer(node.name, node.url))}.$name,\n'; 42 | } 43 | 44 | final fallback = node.fallbackValue != null 45 | ? buildCase('_', node.fallbackValue!) 46 | : ' _ => decoder.expect(\'${node.name} of ${cases.map((v) => v.$1).join(', ').replaceAll("'", '')}\'),\n'; 47 | 48 | return 'return switch (value) {\n' 49 | '${cases.map((v) => buildCase(v.$1, v.$2)).join()}' 50 | '$fallback' 51 | '};'; 52 | })); 53 | }); 54 | } 55 | 56 | b.methods.add(Method((b) { 57 | b.name = '_decodeString'; 58 | b.returns = refer(node.name, node.url); 59 | b.requiredParameters.add(Parameter((b) { 60 | b.name = 'value'; 61 | b.type = refer('String?', dartCoreUrl); 62 | })); 63 | b.requiredParameters.add(Parameter((b) { 64 | b.name = 'decoder'; 65 | b.type = refer('Decoder', codableCoreUrl); 66 | })); 67 | b.body = buildDecodeBody([ 68 | ...node.values.where((v) => v.stringValue != null).map((v) => ('\'${v.stringValue!}\'', v.name)), 69 | if (node.nullValue != null) ('null', node.nullValue!), 70 | ]); 71 | })); 72 | 73 | b.methods.add(Method((b) { 74 | b.name = '_decodeInt'; 75 | b.returns = refer(node.name, node.url); 76 | b.requiredParameters.add(Parameter((b) { 77 | b.name = 'value'; 78 | b.type = refer('int?', dartCoreUrl); 79 | })); 80 | b.requiredParameters.add(Parameter((b) { 81 | b.name = 'decoder'; 82 | b.type = refer('Decoder', codableCoreUrl); 83 | })); 84 | b.body = buildDecodeBody([ 85 | ...node.values.map((v) => (v.intValue.toString(), v.name)), 86 | if (node.nullValue != null) ('null', node.nullValue!), 87 | ]); 88 | })); 89 | 90 | if (!node.isSelfEncodable) { 91 | b.methods.add(Method((b) { 92 | b.name = 'encode'; 93 | b.returns = refer('void'); 94 | b.annotations.add(refer('override', dartCoreUrl)); 95 | b.requiredParameters.add(Parameter((b) { 96 | b.name = 'value'; 97 | b.type = refer(node.name, node.url); 98 | })); 99 | b.requiredParameters.add(Parameter((b) { 100 | b.name = 'encoder'; 101 | b.type = refer('Encoder', codableCoreUrl); 102 | })); 103 | b.body = Block((b) { 104 | b.statements.add(Code.scope((a) => ''' 105 | if (encoder.isHumanReadable()) { 106 | encoder.encodeStringOrNull(switch (value) { 107 | ${node.values.map((v) => ' ${a(refer(node.name, node.url))}.${v.name} => \'${v.stringValue}\';').join('\n')} 108 | ${node.nullValue != null ? 'case ${a(refer(node.name, node.url))}.${node.nullValue!}: return null;' : ''} 109 | default: throw Exception('Unknown value for ${node.name}'); 110 | }); 111 | } else { 112 | encoder.encodeIntOrNull(switch (value) { 113 | ${node.values.map((v) => 'case ${node.name}.${v.name}: return ${v.intValue};').join('\n')} 114 | ${node.nullValue != null ? 'case ${node.name}.${node.nullValue!}: return null;' : ''} 115 | default: throw Exception('Unknown value for ${node.name}'); 116 | }); 117 | } 118 | ''')); 119 | }); 120 | })); 121 | } 122 | }); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/src/common/object.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | import 'package:codable_dart/extended.dart'; 3 | 4 | /// A [Codable] that can encode and decode standard Dart objects (Maps, Lists, etc.). 5 | class ObjectCodable implements Codable, LazyDecodable { 6 | const ObjectCodable(); 7 | 8 | static SelfEncodable wrap(Object? value) { 9 | return SelfEncodable.fromHandler((encoder) { 10 | encoder.encodeObject(value, using: const ObjectCodable()); 11 | }); 12 | } 13 | 14 | @override 15 | Object? decode(Decoder decoder) { 16 | return switch (decoder.whatsNext()) { 17 | DecodingType.keyed || DecodingType.mapped || DecodingType.map => _decodeMap(decoder), 18 | DecodingType.list || DecodingType.iterated => _decodeList(decoder), 19 | DecodingType.string => decoder.decodeString(), 20 | DecodingType.int => decoder.decodeInt(), 21 | DecodingType.double => decoder.decodeDouble(), 22 | DecodingType.bool => decoder.decodeBool(), 23 | DecodingType.nil => (decoder.decodeIsNull(), null).$2, 24 | _ => decoder.decodeObjectOrNull(), 25 | }; 26 | } 27 | 28 | Map _decodeMap(Decoder decoder) { 29 | final map = {}; 30 | var keyed = decoder.decodeKeyed(); 31 | for (Object? key; (key = keyed.nextKey()) != null;) { 32 | map[key.toString()] = decoder.decodeObject(using: this); 33 | } 34 | return map; 35 | } 36 | 37 | List _decodeList(Decoder decoder) { 38 | final list = []; 39 | var iterated = decoder.decodeIterated(); 40 | for (; iterated.nextItem();) { 41 | list.add(decoder.decodeObject(using: this)); 42 | } 43 | return list; 44 | } 45 | 46 | @override 47 | void decodeLazy(LazyDecoder decoder, void Function(Object? value) resolve) { 48 | decoder.whatsNext((type) { 49 | switch (type) { 50 | case DecodingType.keyed || DecodingType.mapped || DecodingType.map: 51 | _decodeLazyMap(decoder, resolve); 52 | case DecodingType.list || DecodingType.iterated: 53 | _decodeLazyList(decoder, resolve); 54 | default: 55 | decoder.decodeObjectOrNull(resolve); 56 | } 57 | }); 58 | } 59 | 60 | void _decodeLazyMap(LazyDecoder decoder, void Function(Object? value) resolve) { 61 | final map = {}; 62 | decoder.decodeKeyed((key, keyed) { 63 | keyed.decodeObjectOrNull((value) { 64 | map[key.toString()] = value; 65 | }, using: this); 66 | }, done: () { 67 | resolve(map); 68 | }); 69 | } 70 | 71 | void _decodeLazyList(LazyDecoder decoder, void Function(Object? value) resolve) { 72 | final list = []; 73 | decoder.decodeIterated((iterated) { 74 | iterated.decodeObjectOrNull((value) { 75 | list.add(value); 76 | }, using: this); 77 | }, done: () { 78 | resolve(list); 79 | }); 80 | } 81 | 82 | @override 83 | void encode(Object? value, Encoder encoder) { 84 | if (value is Map) { 85 | encoder.encodeMap(value, valueUsing: this); 86 | } else if (value is List) { 87 | encoder.encodeIterable(value, using: this); 88 | } else if (value is String) { 89 | encoder.encodeString(value); 90 | } else if (value is int) { 91 | encoder.encodeInt(value); 92 | } else if (value is double) { 93 | encoder.encodeDouble(value); 94 | } else if (value is bool) { 95 | encoder.encodeBool(value); 96 | } else if (value == null) { 97 | encoder.encodeNull(); 98 | } else { 99 | encoder.encodeObject(value); 100 | } 101 | } 102 | } 103 | 104 | extension AsNullableCodable on Codable { 105 | /// Returns a [Codable] that can encode and decode [T] or null. 106 | Codable get orNull => OrNullCodable(this); 107 | } 108 | 109 | extension AsNullableDecodable on Decodable { 110 | /// Returns a [Decodable] object that can decode [T] or null. 111 | Decodable get orNull => OrNullDecodable(this); 112 | } 113 | 114 | extension AsNullableListEncodable on Encodable { 115 | /// Returns an [Encodable] that can encode [T] or null. 116 | Encodable get orNull => OrNullEncodable(this); 117 | } 118 | 119 | /// A [Codable] that can encode and decode [T] or null. 120 | /// 121 | /// Prefer using [AsNullableCodable.orNull] instead of the constructor. 122 | class OrNullCodable with _OrNullDecodable implements Codable, ComposedDecodable { 123 | const OrNullCodable(this.codable); 124 | 125 | @override 126 | final Codable codable; 127 | 128 | @override 129 | void encode(T? value, Encoder encoder) { 130 | encoder.encodeObjectOrNull(value, using: codable); 131 | } 132 | 133 | @override 134 | R unwrap(R Function(Decodable? codableA) fn) { 135 | return fn(codable); 136 | } 137 | } 138 | 139 | /// A [Decodable] implementation that can decode [T] or null. 140 | /// 141 | /// Prefer using [AsNullableDecodable.orNull] instead of the constructor. 142 | class OrNullDecodable with _OrNullDecodable implements ComposedDecodable { 143 | const OrNullDecodable(this.codable); 144 | 145 | @override 146 | final Decodable codable; 147 | } 148 | 149 | /// An [Encodable] that can encode [T] or null. 150 | /// 151 | /// Prefer using [AsNullableEncodable.orNull] instead of the constructor. 152 | class OrNullEncodable implements Encodable { 153 | const OrNullEncodable(this.codable); 154 | 155 | final Encodable codable; 156 | 157 | @override 158 | void encode(T? value, Encoder encoder) { 159 | encoder.encodeObjectOrNull(value, using: codable); 160 | } 161 | } 162 | 163 | mixin _OrNullDecodable implements ComposedDecodable { 164 | Decodable get codable; 165 | 166 | @override 167 | T? decode(Decoder decoder) { 168 | return switch (decoder.whatsNext()) { 169 | DecodingType.nil => decoder.decodeIsNull() ? null : decoder.expect('null'), 170 | _ => codable.decode(decoder), 171 | }; 172 | } 173 | 174 | @override 175 | R unwrap(R Function(Decodable? codableA) fn) { 176 | return fn(codable); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /test/basic/basic_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:codable_dart/extended.dart'; 4 | import 'package:codable_dart/json.dart'; 5 | import 'package:codable_dart/msgpack.dart'; 6 | import 'package:codable_dart/standard.dart'; 7 | import 'package:test/test.dart'; 8 | 9 | import '../utils.dart'; 10 | import 'model/person.dart'; 11 | import 'test_data.dart'; 12 | 13 | void main() { 14 | group("basic model", () { 15 | // Person to compare against. 16 | final expectedPerson = PersonRaw.fromMapRaw(personTestData); 17 | 18 | test("decodes from map", () { 19 | // Uses the fromMap extension on Decodable to decode the map. 20 | Person p = Person.codable.fromMap(personTestData); 21 | expect(p, equals(expectedPerson)); 22 | }); 23 | 24 | test("encodes to map", () { 25 | // Uses the toMap extension on SelfEncodable to encode the map. 26 | final Map encoded = expectedPerson.toMap(); 27 | expect(encoded, equals(personTestData)); 28 | }); 29 | 30 | test("decodes from json", () { 31 | // Uses the fromJson extension on Decodable to decode the json string. 32 | Person p = Person.codable.fromJson(personTestJson); 33 | expect(p, equals(expectedPerson)); 34 | }); 35 | 36 | test("encodes to json", () { 37 | // Uses the toJson extension on SelfEncodable to encode the json string. 38 | final String encoded = expectedPerson.toJson(); 39 | expect(encoded, equals(personTestJson)); 40 | }); 41 | 42 | test("decodes from json bytes", () { 43 | // Uses the fromJsonBytes extension on Decodable to decode the json bytes. 44 | Person p = Person.codable.fromJsonBytes(personTestJsonBytes); 45 | expect(p, equals(expectedPerson)); 46 | }); 47 | 48 | test("encodes to json bytes", () { 49 | // Uses the toJsonBytes extension on SelfEncodable to encode the json bytes. 50 | final List encoded = expectedPerson.toJsonBytes(); 51 | expect(encoded, equals(personTestJsonBytes)); 52 | }); 53 | 54 | test('decodes from msgpack bytes', () { 55 | // Uses the fromMsgPackBytes extension on Decodable to decode the msgpack bytes. 56 | Person p = Person.codable.fromMsgPack(personTestMsgpackBytes); 57 | expect(p, equals(expectedPerson)); 58 | }); 59 | 60 | test('encodes to msgpack bytes', () { 61 | // Uses the toMsgPackBytes extension on SelfEncodable to encode the msgpack bytes. 62 | final List encoded = expectedPerson.toMsgPack(); 63 | expect(encoded, equals(personTestMsgpackBytes)); 64 | }); 65 | 66 | group("using codec", () { 67 | test("decodes from map", () { 68 | // Uses the standard codec to decode the person from a map. 69 | Person p = Person.codable.codec.decode(personTestData); 70 | expect(p, equals(expectedPerson)); 71 | }); 72 | 73 | test("encodes to map", () { 74 | // Uses the standard codec to encode the person, and casts to a map. 75 | final Map encoded = Person.codable.codec.encode(expectedPerson) as Map; 76 | expect(encoded, equals(personTestData)); 77 | }); 78 | 79 | test("decodes from json", () { 80 | // Uses the json codec to decode the person from a String. 81 | Person p = Person.codable.codec.fuse(json).decode(personTestJson); 82 | expect(p, equals(expectedPerson)); 83 | }); 84 | 85 | test("encodes to json", () { 86 | // Uses the json codec to encode the person to a String. 87 | final String encoded = Person.codable.codec.fuse(json).encode(expectedPerson); 88 | expect(encoded, equals(personTestJson)); 89 | }); 90 | 91 | test("decodes from json bytes", () { 92 | // Uses the json codec to decode the person from bytes. 93 | Person p = Person.codable.codec.fuse(json).fuse(utf8).decode(personTestJsonBytes); 94 | expect(p, equals(expectedPerson)); 95 | }); 96 | 97 | test("encodes to json bytes", () { 98 | // Uses the json codec to encode the person to bytes. 99 | final List encoded = Person.codable.codec.fuse(json).fuse(utf8).encode(expectedPerson); 100 | expect(encoded, equals(personTestJsonBytes)); 101 | }); 102 | 103 | test('decodes from msgpack bytes', () { 104 | // Uses the msgpack codec to decode the Person from bytes. 105 | Person p = Person.codable.codec.fuse(msgPack).decode(personTestMsgpackBytes); 106 | expect(p, equals(expectedPerson)); 107 | }); 108 | 109 | test('encodes to msgpack bytes', () { 110 | // Uses the msgpack codec to encode the Person to bytes. 111 | final List encoded = Person.codable.codec.fuse(msgPack).encode(expectedPerson); 112 | expect(encoded, equals(personTestMsgpackBytes)); 113 | }); 114 | 115 | test('decodes chunked json', () async { 116 | final stream = 117 | Person.codable.codec.fuse(json).fuse(utf8).decoder.bind(streamData(personTestJsonBytes, splitEveryN: 100)); 118 | 119 | final Person p = await stream.single; 120 | expect(p, equals(expectedPerson)); 121 | }); 122 | }); 123 | 124 | group('lazily', () { 125 | test("decodes from stream", () async { 126 | final Person p = await Person.codable.fromJsonStream(streamData(personTestJsonBytes, splitEveryN: 100)); 127 | expect(p, equals(expectedPerson)); 128 | }); 129 | 130 | test("decodes from iterated stream", () async { 131 | final stream = Person.codable.stream().fromJsonStream(streamData(personListTestJsonBytes, splitEveryN: 100)); 132 | await stream.listen((p) { 133 | expect(p, equals(expectedPerson)); 134 | }).asFuture(); 135 | }); 136 | 137 | test("decodes with stream codec", () async { 138 | final stream = Person.codable.codec 139 | .fuse(json) 140 | .fuse(utf8) 141 | .decoder 142 | .bind(streamData(personTestJsonBytes, splitEveryN: 100)); 143 | await stream.listen((p) { 144 | expect(p, equals(expectedPerson)); 145 | }).asFuture(); 146 | }); 147 | 148 | test("decodes with chunked codec", () { 149 | final sink = Person.codable.codec.fuse(json).fuse(utf8).decoder.startChunkedConversion(CallbackSink((p) { 150 | expect(p, equals(expectedPerson)); 151 | })); 152 | chunked(personTestJsonBytes, sink); 153 | }); 154 | }); 155 | }); 156 | } 157 | -------------------------------------------------------------------------------- /lib/src/extended/reference.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | import 'package:codable_dart/extended.dart'; 3 | 4 | extension ReferenceDecoder on Decoder { 5 | /// Decodes a [Reference] of [T] using the provided [Decodable]. 6 | Reference decodeReference({Decodable? using}) { 7 | final next = whatsNext(); 8 | if (next is DecodingType || next is DecodingType || next is DecodingType) { 9 | return decodeObject>(using: ReferenceDecodable._(using: using)); 10 | } else { 11 | return Reference(decodeObject(using: using)); 12 | } 13 | } 14 | 15 | /// Decodes a [Reference] of [T] or null using the provided [Decodable]. 16 | Reference? decodeReferenceOrNull({Decodable? using}) { 17 | if (decodeIsNull()) { 18 | return null; 19 | } 20 | return decodeReference(using: using); 21 | } 22 | } 23 | 24 | extension ReferenceMappedDecoder on MappedDecoder { 25 | /// Decodes a [Reference] of [T] using the provided [Decodable]. 26 | Reference decodeReference(String key, {int? id, Decodable? using}) { 27 | final next = whatsNext(key, id: id); 28 | if (next is DecodingType || next is DecodingType || next is DecodingType) { 29 | return decodeObject>(key, id: id, using: ReferenceDecodable._(using: using)); 30 | } else { 31 | return Reference(decodeObject(key, id: id, using: using)); 32 | } 33 | } 34 | 35 | /// Decodes a [Reference] of [T] or null using the provided [Decodable]. 36 | Reference? decodeReferenceOrNull(String key, {int? id, Decodable? using}) { 37 | if (decodeIsNull(key, id: id)) { 38 | return null; 39 | } 40 | return decodeReference(key, id: id, using: using); 41 | } 42 | } 43 | 44 | extension ReferenceEncoder on Encoder { 45 | /// Encodes a [Reference] of [T] using the provided [Encodable]. 46 | void encodeReference(T value, {Encodable? using}) { 47 | assert(canEncodeCustom(), '$runtimeType does not support encoding References.'); 48 | encodeObject(Reference(value, using: using)); 49 | } 50 | 51 | /// Encodes a [Reference] of [T] or null using the provided [Encodable]. 52 | void encodeReferenceOrNull(T? value, {Encodable? using}) { 53 | if (value == null) { 54 | encodeNull(); 55 | } else { 56 | encodeReference(value, using: using); 57 | } 58 | } 59 | } 60 | 61 | extension ReferenceKeyedEncoder on KeyedEncoder { 62 | /// Encodes a [Reference] of [T] using the provided [Encodable]. 63 | void encodeReference(String key, T value, {Encodable? using}) { 64 | assert(canEncodeCustom(), '$runtimeType does not support encoding References.'); 65 | encodeObject(key, Reference(value, using: using)); 66 | } 67 | 68 | /// Encodes a [Reference] of [T] or null using the provided [Encodable]. 69 | void encodeReferenceOrNull(String key, T? value, {Encodable? using}) { 70 | if (value == null) { 71 | encodeNull(key); 72 | } else { 73 | encodeReference(key, value, using: using); 74 | } 75 | } 76 | } 77 | 78 | sealed class Reference { 79 | factory Reference(T value, {Encodable? using}) = _ValueReference; 80 | factory Reference.late({Encodable? using}) = _LateReference; 81 | 82 | Encodable? get using; 83 | 84 | Reference get(R Function(T value) callback); 85 | void set(T value); 86 | 87 | Object? get sentinel; 88 | } 89 | 90 | class _ValueReference implements Reference { 91 | _ValueReference(this._value, {this.using}); 92 | 93 | T _value; 94 | 95 | @override 96 | final Encodable? using; 97 | 98 | @override 99 | Reference get(R Function(T value) callback) { 100 | return Reference(callback(_value)); 101 | } 102 | 103 | @override 104 | void set(T value) { 105 | _value = value; 106 | } 107 | 108 | @override 109 | Object? get sentinel => _value; 110 | 111 | @override 112 | bool operator ==(Object other) { 113 | if (identical(this, other)) return true; 114 | return (other is _ValueReference && _value == other._value) || 115 | (other is _LateReference && other._isSet && _value == other._value); 116 | } 117 | 118 | @override 119 | int get hashCode => _value.hashCode; 120 | 121 | @override 122 | String toString() { 123 | return 'Reference<$T>($_value)'; 124 | } 125 | } 126 | 127 | class _LateReference implements Reference { 128 | _LateReference({this.using}); 129 | 130 | T? _value; 131 | bool _isSet = false; 132 | final Set _callbacks = {}; 133 | 134 | @override 135 | final Encodable? using; 136 | 137 | @override 138 | Reference get(R Function(T value) callback) { 139 | if (_isSet) { 140 | return Reference(callback(_value as T)); 141 | } else { 142 | final late = Reference.late(); 143 | _callbacks.add((value) { 144 | late.set(callback(value)); 145 | }); 146 | return late; 147 | } 148 | } 149 | 150 | @override 151 | void set(T value) { 152 | _value = value; 153 | _isSet = true; 154 | for (var callback in _callbacks) { 155 | callback(value); 156 | } 157 | _callbacks.clear(); 158 | } 159 | 160 | @override 161 | Object? get sentinel => _isSet ? _value : this; 162 | 163 | @override 164 | bool operator ==(Object other) { 165 | if (identical(this, other)) return true; 166 | if (!_isSet) return false; 167 | return (other is _LateReference && other._isSet && _value == other._value) || 168 | (other is _ValueReference && _value == other._value); 169 | } 170 | 171 | @override 172 | int get hashCode => _isSet ? _value.hashCode : identityHashCode(this); 173 | 174 | 175 | @override 176 | String toString() { 177 | if (_isSet) { 178 | return 'Reference<$T>($_value)'; 179 | } else { 180 | return 'Reference<$T>.late(not yet set)'; 181 | } 182 | } 183 | } 184 | 185 | final class ReferenceDecodable implements ComposedDecodable1> { 186 | ReferenceDecodable._({this.using}); 187 | 188 | final Decodable? using; 189 | 190 | @override 191 | Reference decode(Decoder decoder) { 192 | throw UnsupportedError('Called "decodeReference()" on a decoder that does not support decoding References.'); 193 | } 194 | 195 | @override 196 | $R extract<$R>($R Function(Decodable? decodableA) fn) { 197 | return fn(using); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /test/mappable/mapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/src/core/interface.dart'; 2 | import 'package:type_plus/type_plus.dart'; 3 | import 'dart:async'; 4 | 5 | import 'package:codable_dart/common.dart'; 6 | import 'package:codable_dart/core.dart'; 7 | import 'package:codable_dart/extended.dart'; 8 | // ignore: implementation_imports 9 | import 'package:type_plus/src/types_registry.dart' show TypeRegistry; 10 | 11 | abstract class Mapper { 12 | const Mapper(); 13 | 14 | /// A unique id for this type, defaults to the name of the type. 15 | /// 16 | /// Override this if you have two types with the same name. 17 | String get id => T.name; 18 | 19 | /// A type factory is what makes generic types work. 20 | Function get typeFactory => (f) => f(); 21 | 22 | /// A getter for the type of this mapper. 23 | Type get type => T; 24 | 25 | bool isFor(dynamic v) => v is T; 26 | bool isForType(Type type) => type.base == T; 27 | } 28 | 29 | abstract interface class CodableMapper implements Mapper { 30 | Codable get codable; 31 | } 32 | 33 | abstract interface class CodableMapper1 implements Mapper { 34 | Codable codable([Codable? codableA]); 35 | } 36 | 37 | abstract interface class CodableMapper2 implements Mapper { 38 | Codable codable([Codable? codableA, Codable? codableB]); 39 | } 40 | 41 | 42 | Decodable findDecodableFor() { 43 | if (T == List || isBounded()) { 44 | final decodable = T.args.call1(() { 45 | return ListCodable(findCodableFor()!); 46 | }); 47 | if (decodable is Decodable) { 48 | return decodable as Decodable; 49 | } 50 | } 51 | return findCodableFor()!; 52 | } 53 | 54 | Codable? findCodableFor() { 55 | final mapper = MapperContainer.current.findByType(); 56 | return getCodableOf(mapper!); 57 | } 58 | 59 | Codable? getCodableOf(Mapper mapper) { 60 | return switch (mapper) { 61 | CodableMapper m => m.codable, 62 | CodableMapper1 m => T.args.call1(() => m.codable(findCodableFor())), 63 | CodableMapper2 m => T.args.call2(() => m.codable(findCodableFor(), findCodableFor())), 64 | _ => null, 65 | } as Codable?; 66 | } 67 | 68 | Encodable? findEncodeFor(T value) { 69 | if (value is SelfEncodable) return null; 70 | 71 | final mapper = MapperContainer.current.findByValue(value); 72 | return getCodableOf(mapper!)!; 73 | } 74 | 75 | extension on List { 76 | R call1(R Function() fn) { 77 | return first.provideTo(fn); 78 | } 79 | 80 | R call2(R Function() fn) { 81 | return first.provideTo(() => this[1].provideTo(() => fn())); 82 | } 83 | } 84 | 85 | R useMappers(R Function() callback, {List? mappers}) { 86 | return runZoned(callback, zoneValues: { 87 | MapperContainer._containerKey: MapperContainer._inherit(mappers: mappers), 88 | }); 89 | } 90 | 91 | class MapperContainer implements TypeProvider { 92 | static final _containerKey = Object(); 93 | static final _root = MapperContainer._({}); 94 | 95 | static MapperContainer get current => Zone.current[_containerKey] as MapperContainer? ?? _root; 96 | 97 | static MapperContainer _inherit({List? mappers}) { 98 | var parent = current; 99 | if (mappers == null) { 100 | return parent; 101 | } 102 | 103 | return MapperContainer._({ 104 | ...parent._mappers, 105 | for (final m in mappers) m.type: m, 106 | }); 107 | } 108 | 109 | MapperContainer._(this._mappers) { 110 | TypeRegistry.instance.register(this); 111 | } 112 | 113 | final Map _mappers; 114 | 115 | final Map _cachedMappers = {}; 116 | final Map _cachedTypeMappers = {}; 117 | 118 | final Map _cachedObjects = {}; 119 | 120 | Mapper? findByType([Type? type]) { 121 | return _mapperForType(type ?? T); 122 | } 123 | 124 | Mapper? findByValue(T value) { 125 | return _mapperForValue(value); 126 | } 127 | 128 | List> findAll() { 129 | return _mappers.values.whereType>().toList(); 130 | } 131 | 132 | Mapper? _mapperForValue(dynamic value) { 133 | var type = value.runtimeType; 134 | if (_cachedMappers[type] != null) { 135 | return _cachedMappers[type]; 136 | } 137 | var baseType = type.base; 138 | if (baseType == UnresolvedType) { 139 | baseType = type; 140 | } 141 | if (_cachedMappers[baseType] != null) { 142 | return _cachedMappers[baseType]; 143 | } 144 | 145 | var mapper = // 146 | // direct type 147 | _mappers[baseType] ?? 148 | // indirect type ie. subtype 149 | _mappers.values.where((m) => m.isFor(value)).firstOrNull; 150 | 151 | if (mapper != null) { 152 | // if (mapper is ClassMapperBase) { 153 | // mapper = mapper.subOrSelfFor(value) ?? mapper; 154 | // } 155 | if (baseType == mapper.type) { 156 | _cachedMappers[baseType] = mapper; 157 | } else { 158 | _cachedMappers[type] = mapper; 159 | } 160 | } 161 | 162 | return mapper; 163 | } 164 | 165 | Mapper? _mapperForType(Type type) { 166 | if (_cachedTypeMappers[type] case var m?) { 167 | return m; 168 | } 169 | var baseType = type.base; 170 | if (baseType == UnresolvedType) { 171 | baseType = type; 172 | } 173 | if (_cachedTypeMappers[baseType] case var m?) { 174 | return m; 175 | } 176 | var mapper = _mappers[baseType] ?? _mappers.values.where((m) => m.isForType(type)).firstOrNull; 177 | 178 | if (mapper != null) { 179 | if (baseType == mapper.type) { 180 | _cachedTypeMappers[baseType] = mapper; 181 | } else { 182 | _cachedTypeMappers[type] = mapper; 183 | } 184 | } 185 | return mapper; 186 | } 187 | 188 | @override 189 | Function? getFactoryById(String id) { 190 | return _mappers.values.where((m) => m.id == id).firstOrNull?.typeFactory; 191 | } 192 | 193 | @override 194 | List getFactoriesByName(String name) { 195 | return [ 196 | ..._mappers.values.where((m) => m.type.name == name).map((m) => m.typeFactory), 197 | ]; 198 | } 199 | 200 | @override 201 | String? idOf(Type type) { 202 | return _mappers[type]?.id; 203 | } 204 | 205 | T? getCached(Object key) { 206 | return _cachedObjects[key] as T?; 207 | } 208 | 209 | void setCached(Object key, T value) { 210 | _cachedObjects[key] = value; 211 | } 212 | } 213 | 214 | 215 | List> findDiscriminatorFor() { 216 | var mappers = MapperContainer.current.findAll(); 217 | return mappers.map((m) => getCodableOf(m)).whereType>().toList(); 218 | } -------------------------------------------------------------------------------- /lib/src/extended/inheritance.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | import 'package:codable_dart/extended.dart'; 3 | 4 | bool isBounded() => _Type() is _Type; 5 | 6 | class _Type {} 7 | 8 | abstract mixin class SuperDecodable implements Decodable { 9 | String? get discriminatorKey; 10 | 11 | /// The set of discriminators for this class. 12 | List get discriminators; 13 | 14 | /// The fallback decode for this class. 15 | T decodeFallback(Decoder decoder) { 16 | throw 'Fallback decode not implemented for $T.'; 17 | } 18 | 19 | @override 20 | T decode(Decoder decoder) { 21 | final discriminator = findDiscriminator(decoder); 22 | if (discriminator != null) { 23 | final decodable = resolveDiscriminator(discriminator); 24 | return decodable.decode(decoder); 25 | } 26 | return decodeFallback(decoder); 27 | } 28 | 29 | Decodable resolveDiscriminator(Discriminator discriminator) { 30 | return discriminator.resolve(); 31 | } 32 | } 33 | 34 | extension SuperDecodableExtension on SuperDecodable { 35 | Discriminator? findDiscriminator(Decoder decoder, [String? currentValue, List? customDiscriminators]) { 36 | final discriminators = [...this.discriminators, ...?customDiscriminators]; 37 | 38 | String? discriminatorValue = currentValue; 39 | if (discriminatorValue == null && discriminatorKey != null) { 40 | discriminatorValue = decoder.clone().decodeMapped().decodeStringOrNull(discriminatorKey!); 41 | } 42 | 43 | for (var d in discriminators) { 44 | final discriminator = d.canDecodable(decoder, discriminatorKey, discriminatorValue); 45 | if (discriminator == null) continue; 46 | return discriminator; 47 | } 48 | 49 | return null; 50 | } 51 | } 52 | 53 | abstract base class Discriminator { 54 | const Discriminator.base(this.value); 55 | 56 | factory Discriminator(Object? value, Decodable Function() resolve) = _Discriminator0; 57 | 58 | static Discriminator arg1(Object? value, Decodable Function(Decodable? d1) resolve) => 59 | _Discriminator1(value, resolve); 60 | static Discriminator arg1Bounded(Object? value, Function resolve) => _Discriminator1(value, resolve); 61 | 62 | static Discriminator arg2( 63 | Object? value, Decodable Function(Decodable? d1, Decodable? d2) resolve) => 64 | _Discriminator2(value, resolve); 65 | static Discriminator arg2Bounded(Object? value, Function resolve) => 66 | _Discriminator2(value, resolve); 67 | 68 | static List> chain( 69 | List> discriminators, Decodable Function(Discriminator d) resolve) { 70 | return [ 71 | for (final d in discriminators) _Discriminator0(d.value, () => resolve(d)), 72 | ]; 73 | } 74 | 75 | static List> chain1( 76 | List> discriminators, Decodable Function(Discriminator d, Decodable? d1) resolve) { 77 | return [ 78 | for (final d in discriminators) _Discriminator1(d.value, (Decodable? d1) => resolve(d, d1)), 79 | ]; 80 | } 81 | 82 | static List> chain2(List> discriminators, 83 | Decodable Function(Discriminator d, Decodable? d1, Decodable? d2) resolve) { 84 | return [ 85 | for (final d in discriminators) 86 | _Discriminator2( 87 | d.value, (Decodable? d1, Decodable? d2) => resolve(d, d1, d2)), 88 | ]; 89 | } 90 | 91 | final Object? value; 92 | } 93 | 94 | extension DiscriminatorApply on Discriminator { 95 | Decodable resolve() { 96 | return _resolve(null, null); 97 | } 98 | 99 | Decodable resolve1(Decodable? d1) { 100 | return _resolve(d1, null); 101 | } 102 | 103 | Decodable resolve2(Decodable? d1, Decodable? d2) { 104 | return _resolve(d1, d2); 105 | } 106 | 107 | Decodable _resolve(Decodable? d1, Decodable? d2) { 108 | Decodable? result; 109 | if (this case _Discriminator0 d) { 110 | result = d._resolve(); 111 | } else if (this case _Discriminator1 d) { 112 | result = d.resolve(d1); 113 | } else if (this case _Discriminator2 d) { 114 | result = d.resolve(d1, d2); 115 | } 116 | 117 | if (result is Decodable) { 118 | return result; 119 | } 120 | throw 'Cannot resolve discriminator to decode type $T. Got ${result.runtimeType}.'; 121 | } 122 | } 123 | 124 | final class _Discriminator0 extends Discriminator { 125 | const _Discriminator0(super.value, this._resolve) : super.base(); 126 | 127 | final Decodable Function() _resolve; 128 | } 129 | 130 | final class _Discriminator1 extends Discriminator { 131 | const _Discriminator1(super.value, this._resolve) : super.base(); 132 | 133 | final Function _resolve; 134 | 135 | Decodable resolve(Decodable? d1) { 136 | final satisfiesA = isBounded(); 137 | if (satisfiesA) { 138 | return _resolve(d1); 139 | } else { 140 | return _resolve<$A>(d1); 141 | } 142 | } 143 | } 144 | 145 | final class _Discriminator2 extends Discriminator { 146 | const _Discriminator2(super.value, this._resolve) : super.base(); 147 | 148 | final Function _resolve; 149 | 150 | Decodable resolve(Decodable? d1, Decodable? d2) { 151 | final satisfiesA = isBounded(); 152 | final satisfiesB = isBounded(); 153 | if (satisfiesA && satisfiesB) { 154 | return _resolve(d1, d2); 155 | } else if (satisfiesA) { 156 | return _resolve(d1, d2); 157 | } else if (satisfiesB) { 158 | return _resolve<$A, B>(d1, d2); 159 | } else { 160 | return _resolve<$A, $B>(d1, d2); 161 | } 162 | } 163 | } 164 | 165 | extension DiscriminatorExtension on Discriminator { 166 | Discriminator? canDecodable(Decoder decoder, String? currentKey, String? currentValue) { 167 | final discriminator = this; 168 | 169 | if (identical(discriminator.value, 'use_as_default')) { 170 | return discriminator; 171 | } 172 | 173 | if (discriminator.value is Function) { 174 | if (discriminator.value case bool Function(Decoder) fn) { 175 | if (fn(decoder.clone())) { 176 | return discriminator; 177 | } else { 178 | return null; 179 | } 180 | } else { 181 | throw AssertionError('Discriminator function must be of type "bool Function(Decoder)".'); 182 | } 183 | } 184 | 185 | if (currentValue == discriminator.value) { 186 | return discriminator; 187 | } 188 | 189 | return null; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /lib/src/common/iterable.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | import 'package:codable_dart/extended.dart'; 3 | 4 | extension AsListCodable on Codable { 5 | /// Returns a [Codable] that can encode and decode a list of [T]. 6 | /// 7 | /// This let's you use any format extensions with lists: 8 | /// ```dart 9 | /// final List people = Person.codable.list().fromJson(...); 10 | /// final String json = Person.codable.list().toJson(people); 11 | /// ``` 12 | Codable> list() => ListCodable(this); 13 | } 14 | 15 | extension AsListDecodable on Decodable { 16 | /// Returns a [Decodable] object that can decode a list of [T]. 17 | Decodable> list() => ListDecodable(this); 18 | } 19 | 20 | extension AsListEncodable on Encodable { 21 | /// Returns an [Encodable] that can encode a list of [T]. 22 | Encodable> list() => ListEncodable(this); 23 | } 24 | 25 | extension AsSetCodable on Codable { 26 | /// Returns a [Codable] that can encode and decode a set of [T]. 27 | /// 28 | /// This let's you use any format extensions with sets: 29 | /// ```dart 30 | /// final Set people = Person.codable.set().fromJson(...); 31 | /// final String json = Person.codable.set().toJson(people); 32 | /// ``` 33 | Codable> set() => SetCodable(this); 34 | } 35 | 36 | extension AsSetDecodable on Decodable { 37 | /// Returns a [Decodable] object that can decode a set of [T]. 38 | Decodable> set() => SetDecodable(this); 39 | } 40 | 41 | extension AsSetEncodable on Encodable { 42 | /// Returns an [Encodable] that can encode a set of [T]. 43 | Encodable> set() => SetEncodable(this); 44 | } 45 | 46 | extension AsIterableEncodable on Iterable { 47 | /// Returns an [Encodable] that can encode an iterable of [T]. 48 | /// 49 | /// This let's you use any format extensions directly on a [List] of [Encodable]s: 50 | /// ```dart 51 | /// final Iterable people = ...; 52 | /// final String json = people.encode.toJson(); 53 | /// ``` 54 | SelfEncodable get encode => IterableSelfEncodable(this); 55 | } 56 | 57 | /// A [Codable] that can encode and decode a list of [E]. 58 | /// 59 | /// Prefer using [AsListCodable.list] instead of the constructor. 60 | class ListCodable with _ListDecodable implements Codable>, ComposedDecodable1> { 61 | const ListCodable(this.codable); 62 | 63 | @override 64 | final Codable codable; 65 | 66 | @override 67 | void encode(List value, Encoder encoder) { 68 | encoder.encodeIterable(value, using: codable); 69 | } 70 | 71 | @override 72 | R extract(R Function(Codable? codableA) fn) { 73 | return fn(codable); 74 | } 75 | } 76 | 77 | /// A [Decodable] implementation that can decode a list of [E]. 78 | /// 79 | /// Prefer using [AsListDecodable.list] instead of the constructor. 80 | class ListDecodable with _ListDecodable implements ComposedDecodable1> { 81 | const ListDecodable(this.codable); 82 | 83 | @override 84 | final Decodable codable; 85 | } 86 | 87 | /// An [Encodable] that can encode a list of [E]. 88 | /// 89 | /// Prefer using [AsListEncodable.list] instead of the constructor. 90 | class ListEncodable implements Encodable> { 91 | const ListEncodable(this.codable); 92 | 93 | final Encodable codable; 94 | 95 | @override 96 | void encode(List value, Encoder encoder) { 97 | encoder.encodeIterable(value, using: codable); 98 | } 99 | } 100 | 101 | mixin _ListDecodable implements ComposedDecodable1>, LazyDecodable> { 102 | Decodable get codable; 103 | 104 | @override 105 | List decode(Decoder decoder) { 106 | return switch (decoder.whatsNext()) { 107 | DecodingType.list || DecodingType.unknown => decoder.decodeList(using: codable), 108 | DecodingType.iterated => [for (final d = decoder.decodeIterated(); d.nextItem();) d.decodeObject(using: codable)], 109 | _ => decoder.expect('list or iterated'), 110 | }; 111 | } 112 | 113 | @override 114 | void decodeLazy(LazyDecoder decoder, void Function(List) resolve) { 115 | final list = []; 116 | decoder.decodeIterated((decoder) { 117 | decoder.decodeObject(using: codable, (value) { 118 | list.add(value); 119 | }); 120 | }, done: () { 121 | resolve(list); 122 | }); 123 | } 124 | 125 | @override 126 | R extract(R Function(Decodable? codableA) fn) { 127 | return fn(codable); 128 | } 129 | } 130 | 131 | /// A [Codable] that can encode and decode a set of [E]. 132 | /// 133 | /// Prefer using [AsSetCodable.set] instead of the constructor. 134 | class SetCodable with _SetDecodable implements Codable>, ComposedDecodable1> { 135 | const SetCodable(this.codable); 136 | 137 | @override 138 | final Codable codable; 139 | 140 | @override 141 | void encode(Set value, Encoder encoder) { 142 | encoder.encodeIterable(value, using: codable); 143 | } 144 | 145 | @override 146 | R extract(R Function(Codable? codableA) fn) { 147 | return fn(codable); 148 | } 149 | } 150 | 151 | /// A [Decodable] implementation that can decode a set of [E]. 152 | /// 153 | /// Prefer using [AsSetDecodable.set] instead of the constructor. 154 | class SetDecodable with _SetDecodable implements ComposedDecodable1> { 155 | const SetDecodable(this.codable); 156 | 157 | @override 158 | final Decodable codable; 159 | } 160 | 161 | /// An [Encodable] that can encode a set of [E]. 162 | /// 163 | /// Prefer using [AsSetEncodable.set] instead of the constructor. 164 | class SetEncodable implements Encodable> { 165 | const SetEncodable(this.codable); 166 | 167 | final Encodable codable; 168 | 169 | @override 170 | void encode(Set value, Encoder encoder) { 171 | encoder.encodeIterable(value, using: codable); 172 | } 173 | } 174 | 175 | mixin _SetDecodable implements ComposedDecodable1> { 176 | Decodable get codable; 177 | 178 | @override 179 | Set decode(Decoder decoder) { 180 | return switch (decoder.whatsNext()) { 181 | DecodingType.list || DecodingType.unknown => decoder.decodeList(using: codable).toSet(), 182 | DecodingType.iterated => {for (final d = decoder.decodeIterated(); d.nextItem();) d.decodeObject(using: codable)}, 183 | _ => decoder.expect('list or iterated'), 184 | }; 185 | } 186 | 187 | @override 188 | R extract(R Function(Decodable? codableA) fn) { 189 | return fn(codable); 190 | } 191 | } 192 | 193 | /// An [Encodable] that can encode an iterable of [T]. 194 | /// 195 | /// Prefer using [AsIterableEncodable.encode] instead of the constructor. 196 | class IterableSelfEncodable implements SelfEncodable { 197 | const IterableSelfEncodable(this.value); 198 | 199 | final Iterable value; 200 | 201 | @override 202 | void encode(Encoder encoder) { 203 | encoder.encodeIterable(value); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /codable_builder/test/generics_test.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:codable_builder/codable_builder.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | import 'utils.dart'; 6 | 7 | void main() { 8 | group('generics', () { 9 | test('should build generic class with one param', () { 10 | final code = builder.buildCodableClass( 11 | CodableClassNode( 12 | name: 'Box', 13 | url: 'package:test/box.dart', 14 | fields: [ 15 | CodableFieldNode(name: 'content', key: 'content', type: ObjectCodableType('T')), 16 | ], 17 | typeParameters: [CodableTypeParameter(name: 'T')], 18 | ), 19 | ); 20 | 21 | expect( 22 | emit(code), 23 | equals('class BoxCodable extends Codable>\n' 24 | ' implements ComposedDecodable1> {\n' 25 | ' const BoxCodable([Codable? codableT])\n' 26 | ' : encodableT = codableT,\n' 27 | ' decodableT = codableT;\n' 28 | '\n' 29 | ' const BoxCodable.of({\n' 30 | ' Encodable? this.encodableT,\n' 31 | ' Decodable? this.decodableT,\n' 32 | ' });\n' 33 | '\n' 34 | ' final Encodable? encodableT;\n' 35 | '\n' 36 | ' final Decodable? decodableT;\n' 37 | '\n' 38 | ' @override\n' 39 | ' R extract(R Function(Decodable?) fn) {\n' 40 | ' return fn(decodableT);\n' 41 | ' }\n' 42 | '\n' 43 | ' @override\n' 44 | ' Box decode(Decoder decoder) {\n' 45 | ' return switch (decoder.whatsNext()) {\n' 46 | ' DecodingType.mapped ||\n' 47 | ' DecodingType.map => _decodeMapped(decoder.decodeMapped()),\n' 48 | ' DecodingType.keyed ||\n' 49 | ' DecodingType.unknown => _decodeKeyed(decoder.decodeKeyed()),\n' 50 | ' _ => decoder.expect(\'mapped or keyed\'),\n' 51 | ' };\n' 52 | ' }\n' 53 | '\n' 54 | ' Box _decodeMapped(MappedDecoder mapped) {\n' 55 | ' return Box(mapped.decodeObject(\'content\', using: decodableT));\n' 56 | ' }\n' 57 | '\n' 58 | ' Box _decodeKeyed(KeyedDecoder keyed) {\n' 59 | ' late T content;\n' 60 | ' for (Object? key; (key = keyed.nextKey()) != null;) {\n' 61 | ' switch (key) {\n' 62 | ' case \'content\':\n' 63 | ' content = keyed.decodeObject(using: decodableT);\n' 64 | ' default:\n' 65 | ' keyed.skipCurrentValue();\n' 66 | ' }\n' 67 | ' }\n' 68 | '\n' 69 | ' return Box(content);\n' 70 | ' }\n' 71 | '\n' 72 | ' @override\n' 73 | ' void encode(Box value, Encoder encoder) {\n' 74 | ' final keyed = encoder.encodeKeyed();\n' 75 | ' keyed.encodeObject(\'content\', value.content, using: encodableT);\n' 76 | ' keyed.end();\n' 77 | ' }\n' 78 | '}\n' 79 | ''), 80 | ); 81 | }); 82 | 83 | test('should build generic class with two params', () { 84 | final code = builder.buildCodableClass( 85 | CodableClassNode( 86 | name: 'Box', 87 | url: 'package:test/box.dart', 88 | fields: [ 89 | CodableFieldNode(name: 'content', key: 'content', type: ObjectCodableType('B')), 90 | CodableFieldNode(name: 'data', key: 'data', type: ObjectCodableType('A')), 91 | ], 92 | typeParameters: [CodableTypeParameter(name: 'A'), 93 | CodableTypeParameter(name: 'B')], 94 | ), 95 | ); 96 | 97 | expect( 98 | emit(code), 99 | equals('class BoxCodable extends Codable>\n' 100 | ' implements ComposedDecodable2> {\n' 101 | ' const BoxCodable([Codable? codableA, Codable? codableB])\n' 102 | ' : encodableA = codableA,\n' 103 | ' decodableA = codableA,\n' 104 | ' encodableB = codableB,\n' 105 | ' decodableB = codableB;\n' 106 | '\n' 107 | ' const BoxCodable.of({\n' 108 | ' Encodable? this.encodableA,\n' 109 | ' Decodable? this.decodableA,\n' 110 | ' Encodable? this.encodableB,\n' 111 | ' Decodable? this.decodableB,\n' 112 | ' });\n' 113 | '\n' 114 | ' final Encodable? encodableA;\n' 115 | '\n' 116 | ' final Decodable? decodableA;\n' 117 | '\n' 118 | ' final Encodable? encodableB;\n' 119 | '\n' 120 | ' final Decodable? decodableB;\n' 121 | '\n' 122 | ' @override\n' 123 | ' R extract(R Function(Decodable?, Decodable?) fn) {\n' 124 | ' return fn(decodableA, decodableB);\n' 125 | ' }\n' 126 | '\n' 127 | ' @override\n' 128 | ' Box decode(Decoder decoder) {\n' 129 | ' return switch (decoder.whatsNext()) {\n' 130 | ' DecodingType.mapped ||\n' 131 | ' DecodingType.map => _decodeMapped(decoder.decodeMapped()),\n' 132 | ' DecodingType.keyed ||\n' 133 | ' DecodingType.unknown => _decodeKeyed(decoder.decodeKeyed()),\n' 134 | ' _ => decoder.expect(\'mapped or keyed\'),\n' 135 | ' };\n' 136 | ' }\n' 137 | '\n' 138 | ' Box _decodeMapped(MappedDecoder mapped) {\n' 139 | ' return Box(\n' 140 | ' mapped.decodeObject(\'content\', using: decodableB),\n' 141 | ' mapped.decodeObject(\'data\', using: decodableA),\n' 142 | ' );\n' 143 | ' }\n' 144 | '\n' 145 | ' Box _decodeKeyed(KeyedDecoder keyed) {\n' 146 | ' late B content;\n' 147 | ' late A data;\n' 148 | ' for (Object? key; (key = keyed.nextKey()) != null;) {\n' 149 | ' switch (key) {\n' 150 | ' case \'content\':\n' 151 | ' content = keyed.decodeObject(using: decodableB);\n' 152 | ' case \'data\':\n' 153 | ' data = keyed.decodeObject(using: decodableA);\n' 154 | ' default:\n' 155 | ' keyed.skipCurrentValue();\n' 156 | ' }\n' 157 | ' }\n' 158 | '\n' 159 | ' return Box(content, data);\n' 160 | ' }\n' 161 | '\n' 162 | ' @override\n' 163 | ' void encode(Box value, Encoder encoder) {\n' 164 | ' final keyed = encoder.encodeKeyed();\n' 165 | ' keyed.encodeObject(\'content\', value.content, using: encodableB);\n' 166 | ' keyed.encodeObject(\'data\', value.data, using: encodableA);\n' 167 | ' keyed.end();\n' 168 | ' }\n' 169 | '}\n' 170 | ''), 171 | ); 172 | }); 173 | }); 174 | } -------------------------------------------------------------------------------- /codable_builder/test/polymorphics_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_builder/codable_builder.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'utils.dart'; 5 | 6 | void main() { 7 | group('polymorphics', () { 8 | test('should build polymorphic class', () { 9 | final code = builder.buildCodableClass( 10 | CodableClassNode( 11 | name: 'Pet', 12 | url: 'package:test/box.dart', 13 | fields: [], 14 | subclasses: [ 15 | CodableSubclassNode( 16 | name: 'Dog', 17 | url: 'package:test/dog.dart', 18 | ), 19 | CodableSubclassNode( 20 | name: 'Cat', 21 | url: 'package:test/cat.dart', 22 | ), 23 | ], 24 | ), 25 | ); 26 | 27 | expect( 28 | emit(code), 29 | equals('class PetCodable extends Codable with SuperDecodable {\n' 30 | ' const PetCodable();\n' 31 | '\n' 32 | ' @override\n' 33 | ' String get discriminatorKey => \'type\';\n' 34 | '\n' 35 | ' @override\n' 36 | ' List> get discriminators => [\n' 37 | ' Discriminator(\'dog\', DogCodable.new),\n' 38 | ' Discriminator(\'cat\', CatCodable.new),\n' 39 | ' ];\n' 40 | '\n' 41 | ' @override\n' 42 | ' Pet decode(Decoder decoder) {\n' 43 | ' return switch (decoder.whatsNext()) {\n' 44 | ' DecodingType.mapped ||\n' 45 | ' DecodingType.map => _decodeMapped(decoder.decodeMapped()),\n' 46 | ' DecodingType.keyed ||\n' 47 | ' DecodingType.unknown => _decodeKeyed(decoder.decodeKeyed()),\n' 48 | ' _ => decoder.expect(\'mapped or keyed\'),\n' 49 | ' };\n' 50 | ' }\n' 51 | '\n' 52 | ' Pet _decodeMapped(MappedDecoder mapped) {\n' 53 | ' return Pet();\n' 54 | ' }\n' 55 | '\n' 56 | ' Pet _decodeKeyed(KeyedDecoder keyed) {\n' 57 | ' for (Object? key; (key = keyed.nextKey()) != null;) {\n' 58 | ' switch (key) {\n' 59 | ' default:\n' 60 | ' keyed.skipCurrentValue();\n' 61 | ' }\n' 62 | ' }\n' 63 | '\n' 64 | ' return Pet();\n' 65 | ' }\n' 66 | '\n' 67 | ' @override\n' 68 | ' void encode(Pet value, Encoder encoder) {\n' 69 | ' final keyed = encoder.encodeKeyed();\n' 70 | '\n' 71 | ' keyed.end();\n' 72 | ' }\n' 73 | '}\n' 74 | ''), 75 | ); 76 | }); 77 | 78 | test('should build polymorphic class with one type arg', () { 79 | final code = builder.buildCodableClass( 80 | CodableClassNode( 81 | name: 'Pet', 82 | url: 'package:test/box.dart', 83 | fields: [], 84 | typeParameters: [CodableTypeParameter(name: 'T')], 85 | subclasses: [ 86 | CodableSubclassNode( 87 | name: 'Dog', 88 | url: 'package:test/dog.dart', 89 | typeParameterMapping: [CodableTypeParameterMapping(name: 'D', derivedIndex: 0)], 90 | ), 91 | CodableSubclassNode( 92 | name: 'Cat', 93 | url: 'package:test/cat.dart', 94 | ), 95 | CodableSubclassNode( 96 | name: 'Snake', 97 | url: 'package:test/snake.dart', 98 | typeParameterMapping: [CodableTypeParameterMapping(name: 'S', derivedIndex: 0, bound: CodableType.num)], 99 | ), 100 | CodableSubclassNode( 101 | name: 'Lizard', 102 | url: 'package:test/lizard.dart', 103 | typeParameterMapping: [ 104 | CodableTypeParameterMapping(name: 'L', derivedIndex: -1, bound: CodableType.int), 105 | CodableTypeParameterMapping(name: 'T', derivedIndex: 0), 106 | ], 107 | ), 108 | ], 109 | ), 110 | ); 111 | 112 | expect( 113 | emit(code), 114 | equals('class PetCodable extends Codable>\n' 115 | ' with SuperDecodable>\n' 116 | ' implements ComposedDecodable1> {\n' 117 | ' const PetCodable([Codable? codableT])\n' 118 | ' : encodableT = codableT,\n' 119 | ' decodableT = codableT;\n' 120 | '\n' 121 | ' const PetCodable.of({\n' 122 | ' Encodable? this.encodableT,\n' 123 | ' Decodable? this.decodableT,\n' 124 | ' });\n' 125 | '\n' 126 | ' final Encodable? encodableT;\n' 127 | '\n' 128 | ' final Decodable? decodableT;\n' 129 | '\n' 130 | ' @override\n' 131 | ' R extract(R Function(Decodable?) fn) {\n' 132 | ' return fn(decodableT);\n' 133 | ' }\n' 134 | '\n' 135 | ' @override\n' 136 | ' String get discriminatorKey => \'type\';\n' 137 | '\n' 138 | ' @override\n' 139 | ' List> get discriminators => [\n' 140 | ' Discriminator.arg1(\'dog\', (Decodable? decodableT) {\n' 141 | ' return DogCodable.of(decodableD: decodableT);\n' 142 | ' }),\n' 143 | ' Discriminator.arg1(\'cat\', (Decodable? decodableT) {\n' 144 | ' return CatCodable();\n' 145 | ' }),\n' 146 | ' Discriminator.arg1Bounded(\'snake\', (\n' 147 | ' Decodable? decodableT,\n' 148 | ' ) {\n' 149 | ' return SnakeCodable.of(decodableS: decodableT);\n' 150 | ' }),\n' 151 | ' Discriminator.arg1(\'lizard\', (Decodable? decodableT) {\n' 152 | ' return LizardCodable.of(decodableT: decodableT);\n' 153 | ' }),\n' 154 | ' ];\n' 155 | '\n' 156 | ' @override\n' 157 | ' Pet decode(Decoder decoder) {\n' 158 | ' return switch (decoder.whatsNext()) {\n' 159 | ' DecodingType.mapped ||\n' 160 | ' DecodingType.map => _decodeMapped(decoder.decodeMapped()),\n' 161 | ' DecodingType.keyed ||\n' 162 | ' DecodingType.unknown => _decodeKeyed(decoder.decodeKeyed()),\n' 163 | ' _ => decoder.expect(\'mapped or keyed\'),\n' 164 | ' };\n' 165 | ' }\n' 166 | '\n' 167 | ' Pet _decodeMapped(MappedDecoder mapped) {\n' 168 | ' return Pet();\n' 169 | ' }\n' 170 | '\n' 171 | ' Pet _decodeKeyed(KeyedDecoder keyed) {\n' 172 | ' for (Object? key; (key = keyed.nextKey()) != null;) {\n' 173 | ' switch (key) {\n' 174 | ' default:\n' 175 | ' keyed.skipCurrentValue();\n' 176 | ' }\n' 177 | ' }\n' 178 | '\n' 179 | ' return Pet();\n' 180 | ' }\n' 181 | '\n' 182 | ' @override\n' 183 | ' void encode(Pet value, Encoder encoder) {\n' 184 | ' final keyed = encoder.encodeKeyed();\n' 185 | '\n' 186 | ' keyed.end();\n' 187 | ' }\n' 188 | '}\n' 189 | ''), 190 | ); 191 | }); 192 | }); 193 | } 194 | -------------------------------------------------------------------------------- /test/basic/model/person.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:codable_dart/core.dart'; 4 | import 'package:codable_dart/src/extended/lazy.dart'; 5 | 6 | class Person with PersonRaw implements SelfEncodable { 7 | Person(this.name, this.age, this.height, this.isDeveloper, this.parent, this.hobbies, this.friends); 8 | 9 | static const LazyCodable codable = PersonCodable(); 10 | 11 | final String name; 12 | final int age; 13 | final double height; 14 | final bool isDeveloper; 15 | final Person? parent; 16 | final List hobbies; 17 | final List friends; 18 | 19 | @override 20 | bool operator ==(Object other) => 21 | identical(this, other) || 22 | other is Person && 23 | runtimeType == other.runtimeType && 24 | name == other.name && 25 | age == other.age && 26 | height == other.height && 27 | isDeveloper == other.isDeveloper && 28 | parent == other.parent && 29 | hobbies.indexed.every((e) => other.hobbies[e.$1] == e.$2) && 30 | friends.indexed.every((e) => other.friends[e.$1] == e.$2); 31 | 32 | @override 33 | int get hashCode => Object.hash(name, age, height, isDeveloper, parent, hobbies, friends); 34 | 35 | @override 36 | String toString() { 37 | return 'Person(name: $name, age: $age, height: $height, isDeveloper: $isDeveloper, parent: $parent, hobbies: $hobbies, friends: $friends)'; 38 | } 39 | 40 | // ====== Codable Code ====== 41 | // Keep in mind that the below code could also easily be generated by macros or a code generator. 42 | 43 | @override 44 | void encode(Encoder encoder) { 45 | encoder.encodeKeyed() 46 | ..encodeString('name', name) 47 | ..encodeInt('age', age) 48 | ..encodeDouble('height', height) 49 | ..encodeBool('isDeveloper', isDeveloper) 50 | ..encodeObjectOrNull('parent', parent) 51 | ..encodeIterable('hobbies', hobbies) 52 | ..encodeIterable('friends', friends) 53 | ..end(); 54 | } 55 | } 56 | 57 | /// Codable implementation for [Person]. 58 | /// 59 | /// This extends the [SelfCodable] class for a default implementation of [encode] and 60 | /// implements the [decode] method. 61 | class PersonCodable extends SelfCodable implements LazyCodable { 62 | const PersonCodable(); 63 | 64 | @override 65 | Person decode(Decoder decoder) { 66 | return switch (decoder.whatsNext()) { 67 | // If the format prefers mapped decoding, use mapped decoding. 68 | DecodingType.mapped || DecodingType.map => decodeMapped(decoder.decodeMapped()), 69 | // If the format prefers keyed decoding or is non-self describing, use keyed decoding. 70 | DecodingType.keyed || DecodingType.unknown => decodeKeyed(decoder.decodeKeyed()), 71 | _ => decoder.expect('mapped or keyed'), 72 | }; 73 | } 74 | 75 | Person decodeKeyed(KeyedDecoder keyed) { 76 | late String name; 77 | late int age; 78 | late double height; 79 | late bool isDeveloper; 80 | Person? parent; 81 | late List hobbies; 82 | late List friends; 83 | 84 | for (Object? key; (key = keyed.nextKey()) != null;) { 85 | switch (key) { 86 | case 'name': 87 | name = keyed.decodeString(); 88 | case 'age': 89 | age = keyed.decodeInt(); 90 | case 'height': 91 | height = keyed.decodeDouble(); 92 | case 'isDeveloper': 93 | isDeveloper = keyed.decodeBool(); 94 | case 'parent': 95 | parent = keyed.decodeObjectOrNull(using: Person.codable); 96 | case 'hobbies': 97 | hobbies = keyed.decodeList(); 98 | case 'friends': 99 | friends = keyed.decodeList(using: Person.codable); 100 | default: 101 | keyed.skipCurrentValue(); 102 | } 103 | } 104 | 105 | return Person(name, age, height, isDeveloper, parent, hobbies, friends); 106 | } 107 | 108 | Person decodeMapped(MappedDecoder mapped) { 109 | return Person( 110 | mapped.decodeString('name'), 111 | mapped.decodeInt('age'), 112 | mapped.decodeDouble('height'), 113 | mapped.decodeBool('isDeveloper'), 114 | mapped.decodeObjectOrNull('parent', using: Person.codable), 115 | mapped.decodeList('hobbies'), 116 | mapped.decodeList('friends', using: Person.codable), 117 | ); 118 | } 119 | 120 | @override 121 | void decodeLazy(LazyDecoder decoder, void Function(Person) resolve) { 122 | late String name; 123 | late int age; 124 | late double height; 125 | late bool isDeveloper; 126 | Person? parent; 127 | late List hobbies; 128 | late List friends; 129 | 130 | void setName(Decoder d) => name = d.decodeString(); 131 | void setAge(Decoder d) => age = d.decodeInt(); 132 | void setHeight(Decoder d) => height = d.decodeDouble(); 133 | void setIsDeveloper(Decoder d) => isDeveloper = d.decodeBool(); 134 | void setParent(Person? value) => parent = value; 135 | void setHobbies(List value) => hobbies = value; 136 | void setFriends(List value) => friends = value; 137 | 138 | decoder.decodeKeyed((key, decoder) { 139 | switch (key) { 140 | case 'name': 141 | decoder.decodeEager(setName); 142 | case 'age': 143 | decoder.decodeEager(setAge); 144 | case 'height': 145 | decoder.decodeEager(setHeight); 146 | case 'isDeveloper': 147 | decoder.decodeEager(setIsDeveloper); 148 | case 'parent': 149 | decoder.decodeObjectOrNull(setParent, using: Person.codable); 150 | case 'hobbies': 151 | decoder.decodeList(setHobbies); 152 | case 'friends': 153 | decoder.decodeList(setFriends, using: Person.codable); 154 | default: 155 | decoder.skipCurrentValue(); 156 | } 157 | }, done: () { 158 | final person = Person(name, age, height, isDeveloper, parent, hobbies, friends); 159 | resolve(person); 160 | }); 161 | } 162 | } 163 | 164 | /// Baseline implementations for encoding and decoding a [Person] instance. 165 | /// 166 | /// This is how we usually encode and decode models in Dart (e.g. code generated by json_serializable). 167 | /// Its used as a baseline against checking performance and correctness of the codable implementation. 168 | mixin PersonRaw { 169 | static Person fromMapRaw(Map map) { 170 | return Person( 171 | map['name'] as String, 172 | (map['age'] as num).toInt(), 173 | (map['height'] as num).toDouble(), 174 | map['isDeveloper'] as bool, 175 | map['parent'] == null ? null : PersonRaw.fromMapRaw(map['parent'] as Map), 176 | (map['hobbies'] as List).cast(), 177 | (map['friends'] as List).map((e) => PersonRaw.fromMapRaw(e as Map)).toList(), 178 | ); 179 | } 180 | 181 | static Person fromJsonRaw(String json) { 182 | return fromMapRaw(jsonDecode(json) as Map); 183 | } 184 | 185 | static Person fromJsonBytesRaw(List json) { 186 | return fromMapRaw(jsonBytes.decode(json) as Map); 187 | } 188 | 189 | Map toMapRaw() { 190 | final value = this as Person; 191 | return { 192 | 'name': value.name, 193 | 'age': value.age, 194 | 'height': value.height, 195 | 'isDeveloper': value.isDeveloper, 196 | 'parent': value.parent?.toMapRaw(), 197 | 'hobbies': value.hobbies, 198 | 'friends': value.friends.map((e) => e.toMapRaw()).toList(), 199 | }; 200 | } 201 | 202 | String toJsonRaw() { 203 | return jsonEncode(toMapRaw()); 204 | } 205 | 206 | List toJsonBytesRaw() { 207 | return jsonBytes.encode(toMapRaw()); 208 | } 209 | } 210 | 211 | final jsonBytes = json.fuse(utf8); 212 | -------------------------------------------------------------------------------- /doc/models.md: -------------------------------------------------------------------------------- 1 | This section goes over how to consume the package from an end-developer perspective. This skips over some details on how the used interfaces are implemented, which is explained in more detail [here](). 2 | 3 | The following shows an example of for de/encoding a `Person` model from different data formats. 4 | 5 | First we define the model by implementing `SelfEncodable`: 6 | 7 | ```dart 8 | import 'package:codable_dart/core.dart'; 9 | 10 | class Person implements SelfEncodable { 11 | Person(this.name, this.age); 12 | 13 | final String name; 14 | final int age; 15 | 16 | @override 17 | void encode(Encoder encoder) { 18 | /* We skip the implementation for now ... */ 19 | } 20 | } 21 | ``` 22 | 23 | We also define a `static const Codable codable` on the `Person` class, which is defined like this: 24 | 25 | ```dart 26 | import 'package:codable_dart/core.dart'; 27 | 28 | class Person implements SelfEncodable { 29 | /* ... */ 30 | 31 | static const Codable codable = PersonCodable(); 32 | 33 | /* ... */ 34 | } 35 | 36 | class PersonCodable extends SelfCodable { 37 | const PersonCodable(); 38 | 39 | @override 40 | Person decode(Decoder decoder) { 41 | return Person( 42 | /* We skip the implementation for now ... */ 43 | ); 44 | } 45 | } 46 | ``` 47 | 48 | The `SelfCodable` class extends `Codable` and therefore implements both the `Encodable` and `Decodable` interfaces. It also uses the `Person`s `encode()` implementation by default and therefore only requires subclasses to implement the `decode()` method. More on this later. 49 | 50 | This is already all we need for the `Person` model to be decoded and encoded to any available data format. We can now deserialize and serialize `Person` like this: 51 | 52 | ```dart 53 | import 'package:codable_dart/json.dart'; 54 | 55 | void main() { 56 | final String source = '{"name":"Kilian Schulte","age":27}'; 57 | 58 | // Deserialize Person from JSON 59 | final Person person = Person.codable.fromJson(source); 60 | 61 | // Serialize Person to JSON 62 | final String json = person.toJson(); 63 | 64 | assert(json == source); 65 | } 66 | ``` 67 | 68 | This works because the `fromJson()` and `toJson()` methods are **extension methods** on `Codable` and `SelfEncodable`. 69 | The convention is that all data format implementations define these extensions. This makes it possible to change formats and methods simply by changing one import: 70 | 71 | ```dart 72 | // Changed from '/json.dart' to '/msgpack.dart' 73 | import 'package:codable_dart/msgpack.dart'; 74 | 75 | void main() { 76 | final Uint8List source = /* binary data */; 77 | 78 | // Deserialize Person from MessagePack 79 | final Person person = Person.codable.fromMsgPack(source); 80 | 81 | // Serialize Person to MessagePack 82 | final Uint8List msgpack = person.toMsgPack(); 83 | 84 | assert(msgpack == source); 85 | } 86 | ``` 87 | 88 | Again, the definitions and implementations of `Person` and `PersonCodable` stay untouched. 89 | 90 | The extension method system of course also works for third-party packages: 91 | 92 | ```dart 93 | // Assuming this uses the codable protocol. 94 | import 'package:yaml/yaml.dart'; 95 | 96 | void main() { 97 | final String source = /* yaml string */; 98 | 99 | // Deserialize Person from Yaml 100 | final Person person = Person.codable.fromYaml(source); 101 | 102 | // Serialize Person to Yaml 103 | final String yaml = person.toYaml(); 104 | 105 | assert(yaml == source); 106 | } 107 | ``` 108 | 109 | # Third-party Types 110 | 111 | Implementing the `SelfEncodable` interface is only possible when you define the model class yourself, which is not the case for core types or third-party types. However you can still add serialization capabilities to these or any type by defining a `Codable` class like this: 112 | 113 | ```dart 114 | class UriCodable extends Codable { 115 | const UriCodable(); 116 | 117 | @override 118 | void encode(Uri value, Encoder encoder) { 119 | /* ... */ 120 | } 121 | 122 | @override 123 | Uri decode(Decoder decoder) { 124 | /* ... */ 125 | } 126 | } 127 | ``` 128 | 129 | The [`UriCodable`](https://github.com/schultek/codable/blob/main/lib/src/common/uri.dart) makes the `Uri` class from `dart:core` serializable using the same extension methods as above: 130 | 131 | ```dart 132 | import 'package:codable_dart/json.dart'; 133 | 134 | void main() { 135 | final String source = '"https://schultek.dev"'; 136 | 137 | // Deserialize Uri from JSON 138 | final Uri uri = const UriCodable().fromJson(source); 139 | 140 | // Serialize Uri to JSON 141 | final String json = const UriCodable().toJson(uri); 142 | 143 | assert(json == source); 144 | } 145 | ``` 146 | 147 | # Collections (List, Set, Map) 148 | 149 | When dealing with collections, the package defines some convenient extension methods that makes working with `List`s, `Set`s and `Map`s of models a lot easier. 150 | 151 | To decode a `List` of models, use the `.list()` extension method on `Codable`. This will return a `Codable>`, which you can use as normal to decode from any data format: 152 | 153 | ```dart 154 | // In one line: 155 | final List persons = Person.codable.list().fromJson('...'); 156 | 157 | // Step by step: 158 | final Codable personCodable = Person.codable; 159 | final Codable personListCodable = personCodable.list(); 160 | final List persons = personListCodable.fromJson('...'); 161 | ``` 162 | 163 | To encode a `List` (or any `Iterable`) of models, use the `.encode` extension getter on `List`. This will return a new `SelfEncodable`, which you can use as normal to encode to any data format: 164 | 165 | ```dart 166 | final List persons = ...; 167 | 168 | // In one line: 169 | final String json = persons.encode.toJson(); 170 | 171 | // Step by step: 172 | final SelfEncodable listEncodable = persons.encode; 173 | final String json = listEncodable.toJson(); 174 | ``` 175 | 176 | This works also with `Set`s and `Map`s: 177 | 178 | ```dart 179 | final Set personsSet = Person.codable.set().fromJson('...'); 180 | final Map personsMap = Person.codable.map().fromJson('...'); 181 | 182 | final String jsonSet = personsSet.encode.toJson(); 183 | final String jsonMap = personsMap.encode.toJson(); 184 | ``` 185 | 186 | Additionally for `Map`s, you can specify a `Codable` to de/encode non-primitive map keys: 187 | 188 | ```dart 189 | final Codable> personMapCodable = Person.codable.map(UriCodable()); 190 | 191 | final Map personByUriMap = personMapCodable.fromJson('...'); 192 | final String json = personMapCodable.toJson(personByUriMap); 193 | ``` 194 | 195 | # Standard Format 196 | 197 | In addition to serial data formats like `JSON`, the protocol also supports a special 'standard' format, that can be used to de/encode models to Dart `Map`s, `List`s and primitive value types. 198 | 199 | _This is the equivalent to what the `toJson()` method of `json_serializable` does._ As explained in the beginning, this is technically not serialization, but since its a very common thing to do, this protocol of course also has support for it. 200 | 201 | The usage is the same as with any other data format, and the methods are named `fromValue()` and `toValue()`. Additionally, because of the common use-case, there is an additional `fromMap()` and `toMap()` that simply cast the value: 202 | 203 | ```dart 204 | import 'package:codable_dart/standard.dart'; 205 | 206 | void main() { 207 | final Map source = {'name': 'Jasper the Dog', 'age': 3}; 208 | 209 | // Decode Person from a Map. 210 | final Person person = Person.codable.fromMap(source); 211 | // Encode Person and cast to a Map. 212 | final Map map = person.toMap(); 213 | 214 | assert(map == source); 215 | 216 | final String url = 'schultek.dev'; 217 | 218 | // Decode Uri from a Dart standard object (e.g. String). 219 | final Uri uri = UriCodable().fromValue(source); 220 | // Encode Uri to a Dart standard object. 221 | final Object? object = UriCodable().toValue(uri); 222 | 223 | assert(object == url); 224 | } 225 | ``` 226 | --------------------------------------------------------------------------------