├── .gitignore ├── CHANGELOG.md ├── README.md ├── analysis_options.yaml ├── dart_test.yaml ├── docs └── rfc.md ├── example └── README.md ├── lib ├── common.dart ├── core.dart ├── csv.dart ├── extended.dart ├── json.dart ├── msgpack.dart ├── src │ ├── codec │ │ ├── codec.dart │ │ ├── converter.dart │ │ ├── csv.dart │ │ ├── json.dart │ │ ├── msgpack.dart │ │ └── standard.dart │ ├── common │ │ ├── datetime.dart │ │ ├── iterable.dart │ │ ├── map.dart │ │ ├── object.dart │ │ └── uri.dart │ ├── core │ │ ├── decoder.dart │ │ ├── encoder.dart │ │ ├── errors.dart │ │ └── interface.dart │ ├── extended │ │ ├── compat.dart │ │ ├── custom.dart │ │ ├── generics.dart │ │ └── inheritance.dart │ └── formats │ │ ├── csv.dart │ │ ├── json.dart │ │ ├── msgpack.dart │ │ └── standard.dart └── standard.dart ├── pubspec.yaml └── test ├── basic ├── basic_test.dart ├── model │ └── person.dart └── test_data.dart ├── benchmark ├── bench.dart └── performance_test.dart ├── collections └── collections_test.dart ├── csv ├── csv_test.dart ├── model │ └── measures.dart └── test_data.dart ├── enum ├── enum_test.dart └── model │ └── color.dart ├── error_handling └── error_handling_test.dart ├── generics └── basic │ ├── basic_test.dart │ └── model │ └── box.dart ├── hooks ├── delegate.dart └── hooks.dart ├── mappable ├── mapper.dart └── simple.dart └── polymorphism ├── basic ├── model │ └── pet.dart └── poly_test.dart ├── complex ├── complex_poly_test.dart └── model │ └── box.dart └── multi_interface ├── model └── material.dart └── multi_interface_test.dart /.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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | - Initial version. 4 | -------------------------------------------------------------------------------- /README.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 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | analyzer: 4 | enable-experiment: 5 | - macros 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | See examples in RFC and test/ folder 2 | -------------------------------------------------------------------------------- /lib/common.dart: -------------------------------------------------------------------------------- 1 | export 'src/common/datetime.dart'; 2 | export 'src/common/uri.dart'; 3 | export 'src/common/iterable.dart'; 4 | export 'src/common/map.dart'; 5 | -------------------------------------------------------------------------------- /lib/core.dart: -------------------------------------------------------------------------------- 1 | /// Protocol for encoding and decoding objects to various data formats. 2 | /// 3 | /// This library contains only the core protocol interfaces and classes. 4 | /// - For reference implementations of common data formats see the `implementation` library. 5 | /// - For extensions, helpers and utilities that make working with the protocol more convenient 6 | /// see the `extended` library. 7 | /// - For a high-level API that combines the protocol with end-user abstractions see the `mapper` library. 8 | library core; 9 | 10 | export 'src/core/interface.dart'; 11 | export 'src/core/decoder.dart'; 12 | export 'src/core/encoder.dart'; 13 | export 'src/core/errors.dart'; 14 | -------------------------------------------------------------------------------- /lib/csv.dart: -------------------------------------------------------------------------------- 1 | export 'src/codec/csv.dart'; 2 | export 'src/formats/csv.dart'; 3 | -------------------------------------------------------------------------------- /lib/extended.dart: -------------------------------------------------------------------------------- 1 | export 'src/extended/compat.dart'; 2 | export 'src/extended/generics.dart'; 3 | export 'src/extended/custom.dart'; 4 | export 'src/extended/inheritance.dart'; 5 | -------------------------------------------------------------------------------- /lib/json.dart: -------------------------------------------------------------------------------- 1 | export 'src/codec/json.dart'; 2 | export 'src/formats/json.dart'; 3 | -------------------------------------------------------------------------------- /lib/msgpack.dart: -------------------------------------------------------------------------------- 1 | export 'src/codec/msgpack.dart'; 2 | export 'src/formats/msgpack.dart'; 3 | -------------------------------------------------------------------------------- /lib/src/codec/codec.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:codable_dart/standard.dart'; 4 | 5 | import '../../core.dart'; 6 | import '../common/object.dart'; 7 | import 'converter.dart'; 8 | 9 | typedef DecodeCallback = R Function(T value, Decodable using); 10 | typedef EncodeCallback = T Function(R value, Encodable using); 11 | 12 | abstract class CodableCodec extends Codec with CodableCompatibleCodec { 13 | const CodableCodec(); 14 | 15 | T performDecode(Out value, {required Decodable using}); 16 | Out performEncode(T value, {required Encodable using}); 17 | 18 | @override 19 | Converter get decoder => CallbackConverter(performDecode, const ObjectCodable()); 20 | 21 | @override 22 | Converter get encoder => CallbackConverter(performEncode, const ObjectCodable()); 23 | 24 | @override 25 | Codec? fuseCodable(Codable codable) { 26 | return _CodableCodec(this, codable); 27 | } 28 | } 29 | 30 | class _CodableCodec extends Codec { 31 | const _CodableCodec(this.codec, this.codable); 32 | 33 | final CodableCodec codec; 34 | final Codable codable; 35 | 36 | @override 37 | Converter get decoder => CallbackConverter(codec.performDecode, codable); 38 | 39 | @override 40 | Converter get encoder => CallbackConverter(codec.performEncode, codable); 41 | 42 | @override 43 | Codec fuse(Codec other) { 44 | final fused = codec.fuse(other); 45 | if (fused is CodableCodec) { 46 | return _CodableCodec(fused, codable); 47 | } 48 | return super.fuse(other); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/codec/converter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | // A simple converter that just calls a callback. 4 | class CallbackConverter extends Converter { 5 | final T Function(S input, {required U using}) _convert; 6 | final U using; 7 | 8 | const CallbackConverter(this._convert, this.using); 9 | 10 | @override 11 | T convert(S input) => _convert(input, using: using); 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/codec/csv.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:codable_dart/core.dart'; 5 | import 'package:codable_dart/csv.dart'; 6 | import 'package:codable_dart/src/codec/codec.dart'; 7 | 8 | const CsvCodec csv = CsvCodec(); 9 | 10 | class CsvCodec extends CodableCodec { 11 | const CsvCodec(); 12 | 13 | @override 14 | T performDecode(String value, {required Decodable using}) { 15 | return CsvDecoder.decode(value, using); 16 | } 17 | 18 | @override 19 | String performEncode(T value, {required Encodable using}) { 20 | return CsvEncoder.encode(value, using: using); 21 | } 22 | 23 | @override 24 | Codec fuse(Codec other) { 25 | if (other is Utf8Codec) { 26 | return _CsvUtf8CodableCodec() as Codec; 27 | } 28 | return super.fuse(other); 29 | } 30 | } 31 | 32 | class _CsvUtf8CodableCodec extends CodableCodec { 33 | @override 34 | T performDecode(Uint8List value, {required Decodable using}) { 35 | return CsvDecoder.decodeBytes(value, using); 36 | } 37 | 38 | @override 39 | Uint8List performEncode(T value, {required Encodable using}) { 40 | return CsvEncoder.encodeBytes(value, using: using); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/codec/json.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert' hide JsonDecoder, JsonEncoder; 2 | 3 | import 'package:codable_dart/core.dart'; 4 | 5 | import '../../json.dart'; 6 | import 'codec.dart'; 7 | 8 | class JsonCodableCodec extends CodableCodec { 9 | const JsonCodableCodec(); 10 | 11 | @override 12 | T performDecode(String value, {required Decodable using}) { 13 | return JsonDecoder.decode(utf8.encode(value), using); 14 | } 15 | 16 | @override 17 | String performEncode(T value, {required Encodable using}) { 18 | return utf8.decode(JsonEncoder.encode(value, using: using)); 19 | } 20 | 21 | @override 22 | Codec fuse(Codec other) { 23 | if (other is Utf8Codec) { 24 | return _JsonBytesCodableCodec() as Codec; 25 | } else { 26 | return super.fuse(other); 27 | } 28 | } 29 | } 30 | 31 | class _JsonBytesCodableCodec extends CodableCodec> { 32 | const _JsonBytesCodableCodec(); 33 | 34 | @override 35 | T performDecode(List value, {required Decodable using}) { 36 | return JsonDecoder.decode(value, using); 37 | } 38 | 39 | @override 40 | List performEncode(T value, {required Encodable using}) { 41 | return JsonEncoder.encode(value, using: using); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/codec/msgpack.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/msgpack.dart'; 2 | import 'package:codable_dart/src/core/interface.dart'; 3 | 4 | import 'codec.dart'; 5 | 6 | const MsgPackCodec msgPack = MsgPackCodec(); 7 | 8 | class MsgPackCodec extends CodableCodec> { 9 | const MsgPackCodec(); 10 | 11 | @override 12 | T performDecode(List value, {required Decodable using}) { 13 | return MsgPackDecoder.decode(value, using); 14 | } 15 | 16 | @override 17 | List performEncode(T value, {required Encodable using}) { 18 | return MsgPackEncoder.encode(value, using: using); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/codec/standard.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:codable_dart/core.dart'; 4 | import 'package:codable_dart/standard.dart'; 5 | 6 | import '../../json.dart'; 7 | import 'converter.dart'; 8 | 9 | class StandardCodableCodec> extends Codec { 10 | final C codable; 11 | 12 | StandardCodableCodec(this.codable); 13 | 14 | @override 15 | Converter get decoder => CallbackConverter(StandardDecoder.decode, codable); 16 | 17 | @override 18 | Converter get encoder => CallbackConverter(StandardEncoder.encode, codable); 19 | 20 | @override 21 | Codec fuse(Codec other) { 22 | if (other is JsonCodec) { 23 | return JsonCodableCodec().fuseCodable(codable) as Codec; 24 | } else if (other is CodableCompatibleCodec) { 25 | return other.fuseCodable(codable) ?? super.fuse(other); 26 | } else { 27 | return super.fuse(other); 28 | } 29 | } 30 | } 31 | 32 | mixin CodableCompatibleCodec on Codec { 33 | Codec? fuseCodable(Codable codable); 34 | } 35 | 36 | extension StandardCodec on Codable { 37 | Codec get codec => StandardCodableCodec>(this); 38 | } 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.encodable.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, E> { 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, E> { 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, E> { 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 | R extract(R Function(Decodable? codableA) fn) { 115 | return fn(codable); 116 | } 117 | } 118 | 119 | /// A [Codable] that can encode and decode a set of [E]. 120 | /// 121 | /// Prefer using [AsSetCodable.set] instead of the constructor. 122 | class SetCodable with _SetDecodable implements Codable>, ComposedDecodable1, E> { 123 | const SetCodable(this.codable); 124 | 125 | @override 126 | final Codable codable; 127 | 128 | @override 129 | void encode(Set value, Encoder encoder) { 130 | encoder.encodeIterable(value, using: codable); 131 | } 132 | 133 | @override 134 | R extract(R Function(Codable? codableA) fn) { 135 | return fn(codable); 136 | } 137 | } 138 | 139 | /// A [Decodable] implementation that can decode a set of [E]. 140 | /// 141 | /// Prefer using [AsSetDecodable.set] instead of the constructor. 142 | class SetDecodable with _SetDecodable implements ComposedDecodable1, E> { 143 | const SetDecodable(this.codable); 144 | 145 | @override 146 | final Decodable codable; 147 | } 148 | 149 | /// An [Encodable] that can encode a set of [E]. 150 | /// 151 | /// Prefer using [AsSetEncodable.set] instead of the constructor. 152 | class SetEncodable implements Encodable> { 153 | const SetEncodable(this.codable); 154 | 155 | final Encodable codable; 156 | 157 | @override 158 | void encode(Set value, Encoder encoder) { 159 | encoder.encodeIterable(value, using: codable); 160 | } 161 | } 162 | 163 | mixin _SetDecodable implements ComposedDecodable1, E> { 164 | Decodable get codable; 165 | 166 | @override 167 | Set decode(Decoder decoder) { 168 | return switch (decoder.whatsNext()) { 169 | DecodingType.list || DecodingType.unknown => decoder.decodeList(using: codable).toSet(), 170 | DecodingType.iterated => {for (final d = decoder.decodeIterated(); d.nextItem();) d.decodeObject(using: codable)}, 171 | _ => decoder.expect('list or iterated'), 172 | }; 173 | } 174 | 175 | @override 176 | R extract(R Function(Decodable? codableA) fn) { 177 | return fn(codable); 178 | } 179 | } 180 | 181 | /// An [Encodable] that can encode an iterable of [T]. 182 | /// 183 | /// Prefer using [AsIterableEncodable.encode] instead of the constructor. 184 | class IterableSelfEncodable implements SelfEncodable { 185 | const IterableSelfEncodable(this.value); 186 | 187 | final Iterable value; 188 | 189 | @override 190 | void encode(Encoder encoder) { 191 | encoder.encodeIterable(value); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /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, K, V> { 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 | -------------------------------------------------------------------------------- /lib/src/common/object.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | 3 | /// A [Codable] that can encode and decode standard Dart objects (Maps, Lists, etc.). 4 | class ObjectCodable implements Codable { 5 | const ObjectCodable(); 6 | 7 | @override 8 | Object? decode(Decoder decoder) { 9 | return switch (decoder.whatsNext()) { 10 | DecodingType.keyed || DecodingType.mapped || DecodingType.map => _decodeMap(decoder), 11 | DecodingType.list || DecodingType.iterated => _decodeList(decoder), 12 | DecodingType.string => decoder.decodeString(), 13 | DecodingType.int => decoder.decodeInt(), 14 | DecodingType.double => decoder.decodeDouble(), 15 | DecodingType.bool => decoder.decodeBool(), 16 | DecodingType.nil => null, 17 | _ => decoder.decodeObjectOrNull(), 18 | }; 19 | } 20 | 21 | Map _decodeMap(Decoder decoder) { 22 | final map = {}; 23 | var keyed = decoder.decodeKeyed(); 24 | for (Object? key; (key = keyed.nextKey()) != null;) { 25 | map[key.toString()] = decoder.decodeObject(using: this); 26 | } 27 | return map; 28 | } 29 | 30 | List _decodeList(Decoder decoder) { 31 | final list = []; 32 | var iterated = decoder.decodeIterated(); 33 | for (; iterated.nextItem();) { 34 | list.add(decoder.decodeObject(using: this)); 35 | } 36 | return list; 37 | } 38 | 39 | @override 40 | void encode(Object? value, Encoder encoder) { 41 | if (value is Map) { 42 | encoder.encodeMap(value, valueUsing: this); 43 | } else if (value is List) { 44 | encoder.encodeIterable(value, using: this); 45 | } else if (value is String) { 46 | encoder.encodeString(value); 47 | } else if (value is int) { 48 | encoder.encodeInt(value); 49 | } else if (value is double) { 50 | encoder.encodeDouble(value); 51 | } else if (value is bool) { 52 | encoder.encodeBool(value); 53 | } else if (value == null) { 54 | encoder.encodeNull(); 55 | } else { 56 | encoder.encodeObject(value); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/src/core/decoder.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core' as core; 2 | import 'dart:core'; 3 | 4 | import 'interface.dart'; 5 | 6 | /// A data format that can decode different types of data. 7 | /// 8 | /// The role of this interface is to convert the encoded data into the supported types. 9 | /// Different implementations of this interface can support different data types and decoding strategies. 10 | /// 11 | /// Decoding is performed by mutual interaction between the [Decoder] and the [Decodable] implementation. 12 | /// 13 | /// - The [Decoder] can provide the actual or preferred [DecodingType] of the encoded data using [whatsNext]. 14 | /// - The [Decodable] implementation can call decoding methods for the requested or expected type, e.g. [decodeString]. 15 | /// 16 | /// A [Decodable] implementation should first use [whatsNext] to determine the type of the encoded data. 17 | /// Then it should use one of the typed `.decode...()` methods of this interface to decode into its target type. 18 | /// If the returned [DecodingType] is not supported, the implementation can use [expect] to throw a detailed error. 19 | /// 20 | /// ```dart 21 | /// /// Example of a [Decodable.decode] implementation: 22 | /// @override 23 | /// Data decode(Decoder decoder) { 24 | /// switch (decoder.whatsNext()) { 25 | /// case DecodingType.string: 26 | /// final value = decoder.decodeString(); 27 | /// return Data.fromString(value); 28 | /// case DecodingType.double: 29 | /// final value = decoder.decodeDouble(); 30 | /// return Data.fromDouble(value); 31 | /// default: 32 | /// decoder.expect('string or double'); 33 | /// } 34 | /// } 35 | /// ``` 36 | /// 37 | /// What possible [DecodingType]s are returned by [whatsNext] depends on the type of data format: 38 | /// 39 | /// - A self-describing format (i.e. the encoded data includes information about the type and shape of the data) can 40 | /// return [DecodingType]s for all supported types. The [Decodable] implementation may choose to call the appropriate 41 | /// decoding method based on the returned [DecodingType]. However, it is not required to do so. 42 | /// 43 | /// Self-describing formats include JSON, YAML, or binary formats like MessagePack and CBOR. 44 | /// 45 | /// - A non-self-describing format may only return [DecodingType.unknown] for all types. In this case, the [Decodable] 46 | /// implementation must choose a appropriate decoding method based on its expected type. 47 | /// 48 | /// Non-self-describing formats include CSV or binary formats like Protobuf or Avro (when separated from schema). 49 | /// 50 | /// Independent of the formats preferred [DecodingType], it should try to decode other types if requested by 51 | /// the [Decodable]. This allows for more flexible decoding strategies and better error handling. 52 | /// For example, a [Decodable] implementation may choose to call [decodeInt] even if [whatsNext] returns [DecodingType.num]. 53 | /// 54 | /// If the [Decoder] is not able to decode the requested type, it should throw an exception with a detailed message. 55 | /// 56 | /// Calling a decoding method is expected to consume the encoded data. Therefore, a [Decodable] implementation should 57 | /// call exactly one of the decoding methods a single time. Never more or less. 58 | abstract interface class Decoder { 59 | /// Returns the actual or preferred [DecodingType] of the encoded data. 60 | /// 61 | /// Self-describing formats may return a [DecodingType] that indicates the type of the encoded data, 62 | /// or the preferred way of decoding it. 63 | /// 64 | /// Non-self-describing formats may only return [DecodingType.unknown] for all types. In this case, the [Decodable] 65 | /// implementation must try the appropriate decoding method based on the expected type. 66 | DecodingType whatsNext(); 67 | 68 | /// Decodes the data as a boolean value. 69 | bool decodeBool(); 70 | 71 | /// Decodes the data as a nullable boolean value. 72 | bool? decodeBoolOrNull(); 73 | 74 | /// Decodes the data as an integer value. 75 | int decodeInt(); 76 | 77 | /// Decodes the data as a nullable integer value. 78 | int? decodeIntOrNull(); 79 | 80 | /// Decodes the data as a double value. 81 | double decodeDouble(); 82 | 83 | /// Decodes the data as a nullable double value. 84 | double? decodeDoubleOrNull(); 85 | 86 | /// Decodes the data as a num value. 87 | num decodeNum(); 88 | 89 | /// Decodes the data as a nullable num value. 90 | num? decodeNumOrNull(); 91 | 92 | /// Decodes the data as a string value. 93 | String decodeString(); 94 | 95 | /// Decodes the data as a nullable string value. 96 | String? decodeStringOrNull(); 97 | 98 | /// Checks if the data is null. 99 | bool decodeIsNull(); 100 | 101 | /// Decodes the data as an object of type [T]. 102 | /// 103 | /// This forwards the decoding to the provided [Decodable] implementation. 104 | /// Otherwise tries to decode the data to a supported primitive type [T]. 105 | /// 106 | /// This should only be called if the format returned a `DecodingType` from [whatsNext] or 107 | /// is otherwise known to support [T] as a custom type. 108 | T decodeObject({Decodable? using}); 109 | 110 | /// Decodes the data as a nullable object of type [T]. 111 | /// 112 | /// When the data is not null behaves like [decodeObject]. 113 | T? decodeObjectOrNull({Decodable?using}); 114 | 115 | /// Decodes the data as a list of elements. 116 | /// 117 | /// Optionally takes a [Decodable] to decode the elements of the list. 118 | List decodeList({Decodable? using}); 119 | 120 | /// Decodes the data as a nullable list of elements. 121 | /// 122 | /// Optionally takes a [Decodable] to decode the elements of the list. 123 | List? decodeListOrNull({Decodable? using}); 124 | 125 | /// Decodes the data as a map of key-value pairs. 126 | /// 127 | /// Optionally takes [Decodable]s to decode the keys and values of the map. 128 | Map decodeMap({Decodable? keyUsing, Decodable? valueUsing}); 129 | 130 | /// Decodes the data as a nullable map of key-value pairs. 131 | /// 132 | /// Optionally takes [Decodable]s to decode the keys and values of the map. 133 | Map? decodeMapOrNull({Decodable? keyUsing, Decodable? valueUsing}); 134 | 135 | /// Decodes the data as an iterated collection of nested data. 136 | IteratedDecoder decodeIterated(); 137 | 138 | /// Decodes the data as a collection of key-value pairs. 139 | /// The pairs are decoded in order of appearance in the encoded data. 140 | KeyedDecoder decodeKeyed(); 141 | 142 | /// Decodes the data as a collection of key-value pairs. 143 | /// The values are accessed and decoded based on the provided key. 144 | MappedDecoder decodeMapped(); 145 | 146 | /// Whether a [Decodable] implementation should expect to decode their human-readable form. 147 | /// 148 | /// Some types have a human-readable form that may be somewhat expensive to construct, as well as a more 149 | /// compact and efficient form. Generally text-based formats like JSON and YAML will prefer to use the 150 | /// human-readable one and binary formats like MessagePack will prefer the compact one. 151 | bool isHumanReadable(); 152 | 153 | /// Creates a new [Decoder] that is a copy of this one. 154 | /// 155 | /// This is useful when a [Decodable] implementation needs to decode data without consuming the original data. 156 | /// For example when checking if a key exists or decoding a value conditionally. 157 | Decoder clone(); 158 | 159 | /// Throws an exception with a detailed message. 160 | Never expect(String expected); 161 | } 162 | 163 | /// A type of data that can be decoded by a [Decoder]. 164 | /// 165 | /// Self-describing formats may return a [DecodingType] from [whatsNext] that indicates the type of the encoded data, 166 | /// or the preferred way of decoding it. 167 | /// 168 | /// Non-self-describing formats may only return [DecodingType.unknown] for all types. In this case, the [Decodable] 169 | /// implementation must try the appropriate decoding method based on the expected type. 170 | final class DecodingType { 171 | const DecodingType._(); 172 | 173 | /// Hints that the data is null. 174 | static const nil = DecodingType._(); 175 | 176 | /// Hints that the data is of type bool. 177 | static const bool = DecodingType._(); 178 | 179 | /// Hints that the data is of type int. 180 | /// 181 | /// [Decoder]s that return this type should also allow calling [decodeNum]. 182 | static const int = DecodingType._(); 183 | 184 | /// Hints that the data is of type double. 185 | /// 186 | /// [Decoder]s that return this type should also allow calling [decodeNum]. 187 | static const double = DecodingType._(); 188 | 189 | /// Hints that the data is of type num. 190 | /// 191 | /// [Decoder]s that return this type should also allow calling [decodeInt] and [decodeDouble]. 192 | static const num = DecodingType._(); 193 | 194 | /// Hints that the data is of type String. 195 | static const string = DecodingType._(); 196 | 197 | /// Hints that the data is a list of elements. 198 | /// 199 | /// [Decoder]s that return this type should also allow calling [decodeIterated]. 200 | static const list = DecodingType._(); 201 | 202 | /// Hints that the data is a map of key-value pairs. 203 | /// 204 | /// [Decoder]s that return this type should also allow calling [decodeKeyed] and [decodeMapped]. 205 | static const map = DecodingType._(); 206 | 207 | /// Hints that the data is an iterated collection of nested data. 208 | /// 209 | /// [Decoder]s that return this type should also allow calling [decodeList]. 210 | static const iterated = DecodingType._(); 211 | 212 | /// Hints that the data is an iterated collection of key-value pairs of nested data. 213 | /// 214 | /// [Decoder]s that return this type should also allow calling [decodeMap] and [decodeMapped]. 215 | static const keyed = DecodingType._(); 216 | 217 | /// Hints that the data is a direct-access collection of key-value pairs of nested data. 218 | /// 219 | /// [Decoder]s that return this type should also allow calling [decodeMap] and [decodeKeyed]. 220 | static const mapped = DecodingType._(); 221 | 222 | /// Hints that the data is a custom type [T]. 223 | /// 224 | /// [Decoder]s that return this type should allow calling [decodeCustom] with [T]. 225 | const DecodingType.custom(); 226 | 227 | /// Hints that the type of the data is unknown. 228 | static const unknown = DecodingType<_Unknown>._(); 229 | } 230 | 231 | class _Unknown {} 232 | 233 | /// A decoder that can decode iterated collections of data. 234 | /// 235 | /// A [Decodable] implementation can iterate over the collection and decode each item individually. 236 | /// Before each item, the implementation must call [nextItem] to move to the next (or initial) item. 237 | /// The implementation must either call a decoding method, or call [skipCurrentItem] before calling [nextItem] again. 238 | /// 239 | /// A [Decodable] implementation can also skip the remaining items in the collection by calling [skipRemainingItems]. 240 | abstract class IteratedDecoder implements Decoder { 241 | /// Moves to the next item in the collection. 242 | /// 243 | /// Returns `true` if there is another item to decode, otherwise `false`. 244 | bool nextItem(); 245 | 246 | /// Skips the current item in the collection. 247 | /// 248 | /// This is useful when the [Decodable] implementation is not interested in the current item. 249 | /// It must be called before calling [nextItem] again if no decoding method is called instead. 250 | void skipCurrentItem(); 251 | 252 | /// Skips the remaining items in the collection. 253 | /// 254 | /// This is useful when the [Decodable] implementation is not interested in the remaining items. 255 | /// It must be called when [nextItem] is not used exhaustively. 256 | void skipRemainingItems(); 257 | 258 | @override 259 | IteratedDecoder clone(); 260 | } 261 | 262 | /// A decoder that can decode iterated collections of key-value pairs of data. 263 | /// 264 | /// A [Decodable] implementation can iterate over the collection and decode each value individually. 265 | /// Before each value, the implementation must call [nextKey] to get the next (or initial) key. 266 | /// The implementation must either call a decoding method, or call [skipCurrentValue] before calling [nextKey] again. 267 | /// 268 | /// A [Decodable] implementation can also skip the remaining key-value pairs in the collection by calling [skipRemainingKeys]. 269 | abstract class KeyedDecoder implements Decoder { 270 | /// Moves to the next key-value pair in the collection and returns the key. 271 | /// 272 | /// Returns `null` if there are no more key-value pairs to decode. 273 | /// 274 | /// The key can be of type [String] or [int] depending on the format. 275 | Object /* String | int */ ? nextKey(); 276 | 277 | /// Skips the current value in the collection. 278 | /// 279 | /// This is useful when the [Decodable] implementation is not interested in the current value. 280 | /// It must be called before calling [nextKey] again if no decoding method is called instead. 281 | void skipCurrentValue(); 282 | 283 | /// Skips the remaining key-value pairs in the collection. 284 | /// 285 | /// This is useful when the [Decodable] implementation is not interested in the remaining key-value pairs. 286 | /// It must be called when [nextKey] is not used exhaustively. 287 | void skipRemainingKeys(); 288 | 289 | @override 290 | KeyedDecoder clone(); 291 | } 292 | 293 | /// A decoder that can decode direct-access collections of key-value pairs of data. 294 | /// 295 | /// A [Decodable] implementation can access and decode each value based on the provided key or id. 296 | /// 297 | /// Formats may choose to use either the [key] or [id] parameter to access the value, as some formats use 298 | /// [String] keys like JSON and others use [int] ids like Protobuf. The [Decodable] implementation should 299 | /// provide values for both. If no id is provided, formats like Protobuf may not work. 300 | /// 301 | /// A [Decodable] implementation can also get all keys of the collection using the [keys] property. 302 | abstract class MappedDecoder { 303 | /// Returns the actual or preferred [DecodingType] of the encoded data for the given key or id. 304 | /// 305 | /// Self-describing formats may return a [DecodingType] that indicates the type of the encoded data, 306 | /// or the preferred way of decoding it. 307 | /// 308 | /// Non-self-describing formats may only return [DecodingType.unknown] for all types. In this case, the [Decodable] 309 | /// implementation must try the appropriate decoding method based on the expected type. 310 | DecodingType whatsNext(String key, {int? id}); 311 | 312 | /// Decodes the data for the given key or id as a boolean value. 313 | bool decodeBool(String key, {int? id}); 314 | 315 | /// Decodes the data for the given key or id as a nullable boolean value. 316 | bool? decodeBoolOrNull(String key, {int? id}); 317 | 318 | /// Decodes the data for the given key or id as an integer value. 319 | int decodeInt(String key, {int? id}); 320 | 321 | /// Decodes the data for the given key or id as a nullable integer value. 322 | int? decodeIntOrNull(String key, {int? id}); 323 | 324 | /// Decodes the data for the given key or id as a double value. 325 | double decodeDouble(String key, {int? id}); 326 | 327 | /// Decodes the data for the given key or id as a nullable double value. 328 | double? decodeDoubleOrNull(String key, {int? id}); 329 | 330 | /// Decodes the data for the given key or id as a num value. 331 | num decodeNum(String key, {int? id}); 332 | 333 | /// Decodes the data for the given key or id as a nullable num value. 334 | num? decodeNumOrNull(String key, {int? id}); 335 | 336 | /// Decodes the data for the given key or id as a string value. 337 | String decodeString(String key, {int? id}); 338 | 339 | /// Decodes the data for the given key or id as a nullable string value. 340 | String? decodeStringOrNull(String key, {int? id}); 341 | 342 | /// Checks if the data for the given key or id is null. 343 | bool decodeIsNull(String key, {int? id}); 344 | 345 | /// Decodes the data for the given key or id as an object of type [T]. 346 | /// 347 | /// This forwards the decoding to the provided [Decodable] implementation. 348 | /// Otherwise tries to decode the data to a supported primitive type [T]. 349 | T decodeObject(String key, {int? id, Decodable? using}); 350 | 351 | /// Decodes the data for the given key or id as a nullable object of type [T]. 352 | /// 353 | /// When the data is not null, this behaves like [decodeObject]. 354 | T? decodeObjectOrNull(String key, {int? id, Decodable? using}); 355 | 356 | /// Decodes the data for the given key or id as a list of elements. 357 | /// 358 | /// Optionally takes a [Decodable] to decode the elements of the list. 359 | List decodeList(String key, {int? id, Decodable? using}); 360 | 361 | /// Decodes the data for the given key or id as a nullable list of elements. 362 | /// 363 | /// Optionally takes a [Decodable] to decode the elements of the list. 364 | List? decodeListOrNull(String key, {int? id, Decodable? using}); 365 | 366 | /// Decodes the data for the given key or id as a map of key-value pairs. 367 | /// 368 | /// Optionally takes [Decodable]s to decode the keys and values of the map. 369 | Map decodeMap(String key, {int? id, Decodable? keyUsing, Decodable? valueUsing}); 370 | 371 | /// Decodes the data for the given key or id as a nullable map of key-value pairs. 372 | /// 373 | /// Optionally takes [Decodable]s to decode the keys and values of the map. 374 | Map? decodeMapOrNull(String key, {int? id, Decodable? keyUsing, Decodable? valueUsing}); 375 | 376 | /// Decodes the data for the given key or id as an iterated collection of nested data. 377 | IteratedDecoder decodeIterated(String key, {int? id}); 378 | 379 | /// Decodes the data for the given key or id as a iterated collection of key-value pairs of nested data. 380 | KeyedDecoder decodeKeyed(String key, {int? id}); 381 | 382 | /// Decodes the data for the given key or id as a direct-access collection of key-value pairs of nested data. 383 | MappedDecoder decodeMapped(String key, {int? id}); 384 | 385 | /// Whether a [Decodable] implementation should expect to decode their human-readable form. 386 | /// 387 | /// Some types have a human-readable form that may be somewhat expensive to construct, as well as a more 388 | /// compact and efficient form. Generally text-based formats like JSON and YAML will prefer to use the 389 | /// human-readable one and binary formats like MessagePack will prefer the compact one. 390 | bool isHumanReadable(); 391 | 392 | /// Returns the keys (or ids) of the collection. 393 | Iterable get keys; 394 | 395 | /// Throws an exception with a detailed message. 396 | Never expect(String key, String expect, {int? id}); 397 | } 398 | -------------------------------------------------------------------------------- /lib/src/core/encoder.dart: -------------------------------------------------------------------------------- 1 | import 'interface.dart'; 2 | 3 | /// A data format that can encode different types of data. 4 | /// 5 | /// The role of this interface is to convert the supported types into encoded data. 6 | /// Different implementations of this interface can support different data types and encoding strategies. 7 | /// 8 | /// Encoding is performed by an [SelfEncodable] or [Encodable] implementation calling one of the typed 9 | /// [Encoder].encode...() methods of this interface. 10 | /// 11 | /// ```dart 12 | /// /// Example of a [Encodable.encode] implementation: 13 | /// @override 14 | /// void encode(Data value, Encoder encoder) { 15 | /// encoder.encodeString(value.stringProperty); 16 | /// } 17 | /// ``` 18 | /// 19 | /// A data format may only support a subset of the encoding types. If the [Encoder] is not able to encode the 20 | /// provided data, it should throw an exception with a detailed message. 21 | /// 22 | /// An [SelfEncodable] or [Encodable] implementation is expected to call exactly one of the encoding methods a single time. 23 | /// Never more or less. 24 | abstract interface class Encoder { 25 | /// Encodes a boolean value. 26 | void encodeBool(bool value); 27 | 28 | /// Encodes a nullable boolean value. 29 | void encodeBoolOrNull(bool? value); 30 | 31 | /// Encodes an integer value. 32 | void encodeInt(int value); 33 | 34 | /// Encodes a nullable integer value. 35 | void encodeIntOrNull(int? value); 36 | 37 | /// Encodes a double value. 38 | void encodeDouble(double value); 39 | 40 | /// Encodes a nullable double value. 41 | void encodeDoubleOrNull(double? value); 42 | 43 | /// Encodes a num value. 44 | void encodeNum(num value); 45 | 46 | /// Encodes a nullable num value. 47 | void encodeNumOrNull(num? value); 48 | 49 | /// Encodes a string value. 50 | void encodeString(String value); 51 | 52 | /// Encodes a nullable string value. 53 | void encodeStringOrNull(String? value); 54 | 55 | /// Encodes 'null'. 56 | void encodeNull(); 57 | 58 | /// Checks if the encoder can encode the custom type [T]. 59 | /// 60 | /// When this method returns `true`, the [encodeObject] method can be called with the type [T]. 61 | bool canEncodeCustom(); 62 | 63 | /// Encodes an object of type [T]. 64 | /// 65 | /// This tries to encode the object using one of the following ways: 66 | /// 1. If the [using] parameter is provided, it forwards the encoding to the provided [Encodable] implementation. 67 | /// 2. If the object is a [SelfEncodable], it calls [SelfEncodable.encode] on the object. 68 | /// 3. If the object is a supported primitive value, it encodes it as such. 69 | /// 4. If none of the above applies, an error is thrown. 70 | /// 71 | /// This should only be called if the format returned `true` from [canEncodeCustom] or 72 | /// is otherwise known to support [T] as a custom type. 73 | void encodeObject(T value, {Encodable? using}); 74 | 75 | /// Encodes a nullable object of type [T]. 76 | /// 77 | /// When the value is not null, this behaves the same as [encodeObject]. 78 | void encodeObjectOrNull(T? value, {Encodable? using}); 79 | 80 | /// Encodes an iterable of [E]. 81 | /// 82 | /// Optionally takes an [Encodable] function to encode each element. 83 | void encodeIterable(Iterable value, {Encodable? using}); 84 | 85 | /// Encodes a nullable iterable of [E]. 86 | /// 87 | /// Optionally takes an [Encodable] function to encode each element. 88 | void encodeIterableOrNull(Iterable? value, {Encodable? using}); 89 | 90 | /// Encodes a map of [K] and [V]. 91 | /// 92 | /// Optionally takes [Encodable] functions to encode each key and value. 93 | void encodeMap(Map value, {Encodable? keyUsing, Encodable? valueUsing}); 94 | 95 | /// Encodes a nullable map of [K] and [V]. 96 | /// 97 | /// Optionally takes [Encodable] functions to encode each key and value. 98 | void encodeMapOrNull(Map? value, {Encodable? keyUsing, Encodable? valueUsing}); 99 | 100 | /// Starts encoding an iterated collection or nested values. 101 | /// 102 | /// The returned [IteratedEncoder] should be used to encode the items in the collection. 103 | /// The [IteratedEncoder.end] method should be called when all items have been encoded. 104 | IteratedEncoder encodeIterated(); 105 | 106 | /// Starts encoding a keyed collection or key-value pairs. 107 | /// 108 | /// The returned [KeyedEncoder] should be used to encode the key-value pairs. 109 | /// The [KeyedEncoder.end] method should be called when all key-value pairs have been encoded. 110 | KeyedEncoder encodeKeyed(); 111 | 112 | /// Whether a [Encodable] implementation should prefer to encode their human-readable form. 113 | /// 114 | /// Some types have both a human-readable form that may be somewhat expensive to construct, as well as a more 115 | /// compact and efficient form. Generally text-based formats like JSON and YAML will prefer to use the 116 | /// human-readable one and binary formats like MessagePack will prefer the compact one. 117 | bool isHumanReadable(); 118 | } 119 | 120 | /// An encoder that supports encoding iterated collections. 121 | /// 122 | /// An [SelfEncodable] or [Encodable] implementation can encode each item by repeatedly calling a encoding method. 123 | /// Some data formats may not support encoding different types of items in the same collection. 124 | /// 125 | /// The [end] method should be called when all items have been encoded. 126 | abstract interface class IteratedEncoder implements Encoder { 127 | /// Ends encoding the iterated collection. 128 | /// 129 | /// No more items should be encoded after this method has been called. 130 | void end(); 131 | } 132 | 133 | /// An encoder that supports encoding key-value pairs. 134 | /// 135 | /// An [SelfEncodable] or [Encodable] implementation can encode each key-value pair by repeatedly calling a encoding method. 136 | /// All data formats should support encoding different types of values in the same collection. 137 | /// 138 | /// Formats may choose to use either the [key] or [id] parameter for encoding, as some formats use 139 | /// [String] keys like JSON and others use [int] ids like Protobuf. The [Encodable] implementation should 140 | /// provide values for both. If no id is provided, formats like Protobuf may not work. 141 | /// 142 | /// The [end] method should be called when all key-value pairs have been encoded. 143 | abstract interface class KeyedEncoder { 144 | /// Encodes a boolean value for the given key or id. 145 | void encodeBool(String key, bool value, {int? id}); 146 | 147 | /// Encodes a nullable boolean value for the given key or id. 148 | void encodeBoolOrNull(String key, bool? value, {int? id}); 149 | 150 | /// Encodes an integer value for the given key or id. 151 | void encodeInt(String key, int value, {int? id}); 152 | 153 | /// Encodes a nullable integer value for the given key or id. 154 | void encodeIntOrNull(String key, int? value, {int? id}); 155 | 156 | /// Encodes a double value for the given key or id. 157 | void encodeDouble(String key, double value, {int? id}); 158 | 159 | /// Encodes a nullable double value for the given key or id. 160 | void encodeDoubleOrNull(String key, double? value, {int? id}); 161 | 162 | /// Encodes a num value for the given key or id. 163 | void encodeNum(String key, num value, {int? id}); 164 | 165 | /// Encodes a nullable num value for the given key or id. 166 | void encodeNumOrNull(String key, num? value, {int? id}); 167 | 168 | /// Encodes a string value for the given key or id. 169 | void encodeString(String key, String value, {int? id}); 170 | 171 | /// Encodes a nullable string value for the given key or id. 172 | void encodeStringOrNull(String key, String? value, {int? id}); 173 | 174 | /// Encodes 'null' for the given key or id. 175 | void encodeNull(String key, {int? id}); 176 | 177 | /// Checks if the encoder can encode the custom type [T]. 178 | /// 179 | /// When this method returns `true`, the [encodeObject] method can be called with the type [T]. 180 | bool canEncodeCustom(); 181 | 182 | /// Encodes an object of type [T] for the given key or id. 183 | /// 184 | /// This tries to encode the object using one of the following ways: 185 | /// 1. If the [using] parameter is provided, it forwards the encoding to the provided [Encodable] implementation. 186 | /// 2. If the object is a [SelfEncodable], it calls [SelfEncodable.encode] on the object. 187 | /// 3. If the object is a supported primitive value, it encodes it as such. 188 | /// 4. If none of the above applies, an error is thrown. 189 | /// 190 | /// This should only be called if the format returned `true` from [canEncodeCustom] or 191 | /// is otherwise known to support [T] as a custom type. 192 | void encodeObject(String key, T value, {int? id, Encodable? using}); 193 | 194 | /// Encodes a nullable object of type [T] for the given key or id. 195 | /// 196 | /// When the value is not null, this behaves the same as [encodeObject]. 197 | void encodeObjectOrNull(String key, T? value, {int? id, Encodable? using}); 198 | 199 | /// Encodes an iterable of [E] for the given key or id. 200 | /// 201 | /// Optionally takes an [Encodable] function to encode each element. 202 | void encodeIterable(String key, Iterable value, {int? id, Encodable? using}); 203 | 204 | /// Encodes a nullable iterable of [E] for the given key or id. 205 | /// 206 | /// Optionally takes an [Encodable] function to encode each element. 207 | void encodeIterableOrNull(String key, Iterable? value, {int? id, Encodable? using}); 208 | 209 | /// Encodes a map of [K] and [V] for the given key or id. 210 | /// 211 | /// Optionally takes [Encodable] functions to encode each key and value. 212 | void encodeMap(String key, Map value, {int? id, Encodable? keyUsing, Encodable? valueUsing}); 213 | 214 | /// Encodes a nullable map of [K] and [V] for the given key or id. 215 | /// 216 | /// Optionally takes [Encodable] functions to encode each key and value. 217 | void encodeMapOrNull(String key, Map? value, {int? id, Encodable? keyUsing, Encodable? valueUsing}); 218 | 219 | /// Starts encoding an iterated collection or nested values for the given key or id. 220 | /// 221 | /// The returned [IteratedEncoder] should be used to encode the items in the collection. 222 | /// The [IteratedEncoder.end] method should be called when all items have been encoded. 223 | IteratedEncoder encodeIterated(String key, {int? id}); 224 | 225 | /// Starts encoding a keyed collection or key-value pairs for the given key or id. 226 | /// 227 | /// The returned [KeyedEncoder] should be used to encode the key-value pairs. 228 | /// The [KeyedEncoder.end] method should be called when all key-value pairs have been encoded. 229 | KeyedEncoder encodeKeyed(String key, {int? id}); 230 | 231 | /// Whether a [Encodable] implementation should prefer to encode their human-readable form. 232 | /// 233 | /// Some types have a human-readable form that may be somewhat expensive to construct, as well as a more 234 | /// compact and efficient form. Generally text-based formats like JSON and YAML will prefer to use the 235 | /// human-readable one and binary formats like MessagePack will prefer the compact one. 236 | bool isHumanReadable(); 237 | 238 | /// Ends encoding the keyed collection. 239 | /// 240 | /// No more key-value pairs should be encoded after this method has been called. 241 | void end(); 242 | } 243 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/src/core/interface.dart: -------------------------------------------------------------------------------- 1 | import 'decoder.dart'; 2 | import 'encoder.dart'; 3 | 4 | /// An object that can encode itself to various data formats. 5 | /// 6 | /// The interface can be used like this: 7 | /// 8 | /// ```dart 9 | /// class Person implements SelfEncodable { 10 | /// /* ... */ 11 | /// 12 | /// @override 13 | /// void encode(Encoder encoder) { 14 | /// /* ... */ 15 | /// } 16 | /// } 17 | /// ``` 18 | /// 19 | /// Objects implementing [SelfEncodable] can be encoded to various data formats using the respective `to()` 20 | /// extension method. A particular method is available by importing the appropriate library or package: 21 | /// 22 | /// ```dart 23 | /// final Person person = ...; 24 | /// 25 | /// // From 'package:codable/json.dart' 26 | /// final String json = person.toJson(); 27 | /// 28 | /// // From 'package:codable/standard.dart' 29 | /// final Map map = person.toMap(); 30 | /// 31 | /// // From imaginary 'package:x/x.dart' 32 | /// final X x = person.toX(); 33 | /// ``` 34 | /// 35 | /// See also: 36 | /// - [Encodable] for encoding values of some type to various data formats. 37 | /// - [Decodable] for decoding values of some type from various data formats. 38 | /// - [Codable] for combining both encoding and decoding of types. 39 | abstract interface class SelfEncodable { 40 | /// Encodes itself using the [encoder]. 41 | /// 42 | /// The implementation should use one of the typed [Encoder]s `.encode...()` methods to encode the value. 43 | /// It is expected to call exactly one of the encoding methods a single time. Never more or less. 44 | void encode(Encoder encoder); 45 | 46 | /// Creates a [SelfEncodable] from a handler function. 47 | factory SelfEncodable.fromHandler(void Function(Encoder encoder) encode) => _SelfEncodableFromHandler(encode); 48 | } 49 | 50 | /// An object that can encode a value of type [T] to various data formats. 51 | /// 52 | /// The interface can be used like this: 53 | /// 54 | /// ```dart 55 | /// class UriEncodable implements Encodable { 56 | /// const UriEncodable(); 57 | /// 58 | /// @override 59 | /// void encode(Uri value, Encoder encoder) { 60 | /// /* ... */ 61 | /// } 62 | /// } 63 | /// ``` 64 | /// 65 | /// The [Encodable] interface can be used for any type, especially ones that cannot be modified to implement [SelfEncodable] 66 | /// instead, like core or third-party types. It can also be used to separate the encoding logic from the type definition. 67 | /// 68 | /// Objects implementing [Encodable] can encode values to various data formats using the respective `to(T value)` 69 | /// extension method. A particular method is available by importing the appropriate library or package: 70 | /// 71 | /// ```dart 72 | /// final Encodable uriEncodable = const UriEncodable(); 73 | /// final Uri uri = ...; 74 | /// 75 | /// // From 'package:codable/json.dart' 76 | /// final String json = uriEncodable.toJson(uri); 77 | /// 78 | /// // From 'package:codable/standard.dart' 79 | /// final Object value = uriEncodable.toValue(uri); 80 | /// 81 | /// // From imaginary 'package:x/x.dart' 82 | /// final X x = uriEncodable.toX(uri); 83 | /// ``` 84 | /// 85 | /// See also: 86 | /// - [SelfEncodable] for self-encoding objects to various data formats. 87 | /// - [Decodable] for decoding values of some type from various data formats. 88 | /// - [Codable] for combining both encoding and decoding of types. 89 | abstract interface class Encodable { 90 | /// Encodes the [value] using the [encoder]. 91 | /// 92 | /// The implementation must use one of the typed [Encoder]s `.encode...()` methods to encode the value. 93 | /// It is expected to call exactly one of the encoding methods a single time. Never more or less. 94 | void encode(T value, Encoder encoder); 95 | 96 | /// Creates an [Encodable] from a handler function. 97 | factory Encodable.fromHandler(void Function(T value, Encoder encoder) encode) => _EncodableFromHandler(encode); 98 | } 99 | 100 | /// An object that can decode a value of type [T] from various data formats. 101 | /// 102 | /// The interface can be used like this: 103 | /// 104 | /// ```dart 105 | /// class PersonDecodable implements Decodable { 106 | /// const PersonDecodable(); 107 | /// 108 | /// @override 109 | /// Person decode(Decoder decoder) { 110 | /// /* ... */ 111 | /// } 112 | /// } 113 | /// ``` 114 | /// 115 | /// The [Decodable] interface can be used for any type, including first-party types, core or third-party types. 116 | /// 117 | /// Objects implementing [Decodable] can decode values to various data formats using the respective `T from(data)` 118 | /// extension method. A particular method is available by importing the appropriate library or package: 119 | /// 120 | /// ```dart 121 | /// final Decodable personDecodable = const PersonDecodable(); 122 | /// 123 | /// // From 'package:codable/json.dart' 124 | /// final Person person = personDecodable.fromJson('...'); 125 | /// 126 | /// // From 'package:codable/standard.dart' 127 | /// final Person person = personDecodable.fromMap({...}); 128 | /// 129 | /// // From imaginary 'package:x/x.dart' 130 | /// final Person person = personDecodable.fromX(x); 131 | /// ``` 132 | /// 133 | /// See also: 134 | /// - [Encodable] for encoding values to various data formats. 135 | /// - [Codable] for combining both encoding and decoding of types. 136 | abstract interface class Decodable { 137 | /// Decodes a value of type [T] using the [decoder]. 138 | /// 139 | /// The implementation should first use [Decoder.whatsNext] to determine the type of the encoded data. 140 | /// Then it should use one of the [Decoder]s `.decode...()` methods to decode into its target type. 141 | /// If the returned [DecodingType] is not supported, the implementation can use [Decoder.expect] to throw a detailed error. 142 | T decode(Decoder decoder); 143 | 144 | /// Creates a [Decodable] from a handler function. 145 | factory Decodable.fromHandler(T Function(Decoder decoder) decode) => _DecodableFromHandler(decode); 146 | } 147 | 148 | /// An object that can both encode and decode a value of type [T] to/from various data formats. 149 | /// 150 | /// It combines both [Encodable] and [Decodable] into a single interface and can be used like this: 151 | /// 152 | /// ```dart 153 | /// class PersonCodable implements Codable { 154 | /// const PersonCodable(); 155 | /// 156 | /// @override 157 | /// void encode(Person value, Encoder encoder) { 158 | /// /* ... */ 159 | /// } 160 | /// 161 | /// @override 162 | /// Person decode(Decoder decoder) { 163 | /// /* ... */ 164 | /// } 165 | /// } 166 | /// ``` 167 | /// 168 | /// Objects implementing [Codable] can encode and decode values to various data formats using the respective 169 | /// `to(T value)` and `T from(data)` extension methods. A particular method is available by 170 | /// importing the appropriate library or package: 171 | /// 172 | /// ```dart 173 | /// final Codable codable = const PersonCodable(); 174 | /// 175 | /// // From 'package:codable/json.dart' 176 | /// final Person person = codable.fromJson('...'); 177 | /// final String json = codable.toJson(person); 178 | /// 179 | /// // From 'package:codable/standard.dart' 180 | /// final Person person = codable.fromMap({...}); 181 | /// final Map map = codable.toMap(person); 182 | /// 183 | /// // From imaginary 'package:x/x.dart' 184 | /// final Person person = codable.fromX(x); 185 | /// final X x = codable.toX(person); 186 | /// ```` 187 | /// 188 | /// See also: 189 | /// - [Encodable] for encoding values of some type to various data formats. 190 | /// - [Decodable] for decoding values of some type from various data formats. 191 | abstract class Codable implements Encodable, Decodable { 192 | const Codable(); 193 | 194 | /// Creates a [Codable] from a pair of handler functions. 195 | factory Codable.fromHandlers({ 196 | required T Function(Decoder decoder) decode, 197 | required void Function(T value, Encoder encoder) encode, 198 | }) => 199 | _CodableFromHandlers(decode, encode); 200 | } 201 | 202 | /// A default [Codable] implementation for [SelfEncodable] types. 203 | /// 204 | /// This should be used as the base class for creating a [Codable] implementation for an [SelfEncodable] type. 205 | /// Only the [decode] method needs to be implemented, as the [encode] method has a default implementation. 206 | /// 207 | /// ```dart 208 | /// class PersonCodable extends SelfCodable { 209 | /// const PersonCodable(); 210 | /// 211 | /// @override 212 | /// Person decode(Decoder decoder) { 213 | /// /* ... */ 214 | /// } 215 | /// } 216 | abstract class SelfCodable implements Codable { 217 | const SelfCodable(); 218 | 219 | @override 220 | void encode(T value, Encoder encoder) { 221 | value.encode(encoder); 222 | } 223 | 224 | /// Creates a [SelfCodable] from a handler function. 225 | factory SelfCodable.fromHandler(T Function(Decoder decoder) decode) => _SelfCodableFromHandler(decode); 226 | } 227 | 228 | /// ======================== 229 | /// === Internal Classes === 230 | /// ======================== 231 | 232 | final class _SelfEncodableFromHandler implements SelfEncodable { 233 | const _SelfEncodableFromHandler(this._encode); 234 | 235 | final void Function(Encoder encoder) _encode; 236 | 237 | @override 238 | void encode(Encoder encoder) => _encode(encoder); 239 | } 240 | 241 | final class _EncodableFromHandler implements Encodable { 242 | const _EncodableFromHandler(this._encode); 243 | 244 | final void Function(T value, Encoder encoder) _encode; 245 | 246 | @override 247 | void encode(T value, Encoder encoder) => _encode(value, encoder); 248 | } 249 | 250 | final class _DecodableFromHandler implements Decodable { 251 | const _DecodableFromHandler(this._decode); 252 | 253 | final T Function(Decoder decoder) _decode; 254 | 255 | @override 256 | T decode(Decoder decoder) => _decode(decoder); 257 | } 258 | 259 | final class _CodableFromHandlers implements Codable { 260 | const _CodableFromHandlers(this._decode, this._encode); 261 | 262 | final T Function(Decoder decoder) _decode; 263 | final void Function(T value, Encoder encoder) _encode; 264 | 265 | @override 266 | T decode(Decoder decoder) => _decode(decoder); 267 | @override 268 | void encode(T value, Encoder encoder) => _encode(value, encoder); 269 | } 270 | 271 | final class _SelfCodableFromHandler extends SelfCodable { 272 | const _SelfCodableFromHandler(this._decode); 273 | 274 | final T Function(Decoder decoder) _decode; 275 | 276 | @override 277 | T decode(Decoder decoder) => _decode(decoder); 278 | } 279 | -------------------------------------------------------------------------------- /lib/src/extended/compat.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | 3 | class CompatMappedDecoder implements MappedDecoder { 4 | CompatMappedDecoder._(this.wrapped, this.decoders); 5 | 6 | static MappedDecoder wrap(KeyedDecoder decoder) { 7 | final map = {}; 8 | for (Object? key; (key = decoder.nextKey()) != null;) { 9 | map[key!] = decoder.clone(); 10 | decoder.skipCurrentValue(); 11 | } 12 | return CompatMappedDecoder._(decoder, map); 13 | } 14 | 15 | final KeyedDecoder wrapped; 16 | final Map decoders; 17 | 18 | @override 19 | DecodingType whatsNext(String key, {int? id}) { 20 | var d = decoders[key] ?? decoders[id]; 21 | return d!.clone().whatsNext(); 22 | } 23 | 24 | @override 25 | bool decodeBool(String key, {int? id}) { 26 | var d = decoders[key] ?? decoders[id]; 27 | return d!.clone().decodeBool(); 28 | } 29 | 30 | @override 31 | bool? decodeBoolOrNull(String key, {int? id}) { 32 | var d = decoders[key] ?? decoders[id]; 33 | return d?.clone().decodeBoolOrNull(); 34 | } 35 | 36 | @override 37 | int decodeInt(String key, {int? id}) { 38 | var d = decoders[key] ?? decoders[id]; 39 | return d!.clone().decodeInt(); 40 | } 41 | 42 | @override 43 | int? decodeIntOrNull(String key, {int? id}) { 44 | var d = decoders[key] ?? decoders[id]; 45 | return d?.clone().decodeIntOrNull(); 46 | } 47 | 48 | @override 49 | double decodeDouble(String key, {int? id}) { 50 | var d = decoders[key] ?? decoders[id]; 51 | return d!.clone().decodeDouble(); 52 | } 53 | 54 | @override 55 | double? decodeDoubleOrNull(String key, {int? id}) { 56 | var d = decoders[key] ?? decoders[id]; 57 | return d?.clone().decodeDoubleOrNull(); 58 | } 59 | 60 | @override 61 | num decodeNum(String key, {int? id}) { 62 | var d = decoders[key] ?? decoders[id]; 63 | return d!.clone().decodeNum(); 64 | } 65 | 66 | @override 67 | num? decodeNumOrNull(String key, {int? id}) { 68 | var d = decoders[key] ?? decoders[id]; 69 | return d?.clone().decodeNumOrNull(); 70 | } 71 | 72 | @override 73 | String decodeString(String key, {int? id}) { 74 | var d = decoders[key] ?? decoders[id]; 75 | return d!.clone().decodeString(); 76 | } 77 | 78 | @override 79 | String? decodeStringOrNull(String key, {int? id}) { 80 | var d = decoders[key] ?? decoders[id]; 81 | return d?.clone().decodeStringOrNull(); 82 | } 83 | 84 | @override 85 | bool decodeIsNull(String key, {int? id}) { 86 | var d = decoders[key] ?? decoders[id]; 87 | return d != null && d.clone().decodeIsNull(); 88 | } 89 | 90 | @override 91 | T decodeObject(String key, {int? id, Decodable? using}) { 92 | var d = decoders[key] ?? decoders[id]; 93 | return d!.clone().decodeObject(using: using); 94 | } 95 | 96 | @override 97 | T? decodeObjectOrNull(String key, {int? id, Decodable? using}) { 98 | var d = decoders[key] ?? decoders[id]; 99 | return d?.clone().decodeObjectOrNull(using: using); 100 | } 101 | 102 | @override 103 | List decodeList(String key, {int? id, Decodable? using}) { 104 | var d = decoders[key] ?? decoders[id]; 105 | return d!.clone().decodeList(using: using); 106 | } 107 | 108 | @override 109 | List? decodeListOrNull(String key, {int? id, Decodable? using}) { 110 | var d = decoders[key] ?? decoders[id]; 111 | return d?.clone().decodeListOrNull(using: using); 112 | } 113 | 114 | @override 115 | Map decodeMap(String key, {int? id, Decodable? keyUsing, Decodable? valueUsing}) { 116 | var d = decoders[key] ?? decoders[id]; 117 | return d!.clone().decodeMap(keyUsing: keyUsing, valueUsing: valueUsing); 118 | } 119 | 120 | @override 121 | Map? decodeMapOrNull(String key, {int? id, Decodable? keyUsing, Decodable? valueUsing}) { 122 | var d = decoders[key] ?? decoders[id]; 123 | return d?.clone().decodeMapOrNull(keyUsing: keyUsing, valueUsing: valueUsing); 124 | } 125 | 126 | @override 127 | IteratedDecoder decodeIterated(String key, {int? id}) { 128 | var d = decoders[key] ?? decoders[id]; 129 | return d!.clone().decodeIterated(); 130 | } 131 | 132 | @override 133 | KeyedDecoder decodeKeyed(String key, {int? id}) { 134 | var d = decoders[key] ?? decoders[id]; 135 | return d!.clone().decodeKeyed(); 136 | } 137 | 138 | @override 139 | MappedDecoder decodeMapped(String key, {int? id}) { 140 | var d = decoders[key] ?? decoders[id]; 141 | return d!.clone().decodeMapped(); 142 | } 143 | 144 | @override 145 | Iterable get keys => decoders.keys; 146 | 147 | @override 148 | Never expect(String key, String expect, {int? id}) { 149 | var d = decoders[key] ?? decoders[id]; 150 | return d!.clone().expect(expect); 151 | } 152 | 153 | @override 154 | bool isHumanReadable() { 155 | return wrapped.isHumanReadable(); 156 | } 157 | } 158 | 159 | class CompatKeyedDecoder implements KeyedDecoder { 160 | CompatKeyedDecoder._(this.decoder) : keys = decoder.keys.iterator; 161 | 162 | static KeyedDecoder wrap(MappedDecoder decoder) { 163 | return CompatKeyedDecoder._(decoder); 164 | } 165 | 166 | final MappedDecoder decoder; 167 | 168 | final Iterator keys; 169 | bool _done = false; 170 | 171 | @override 172 | DecodingType whatsNext() { 173 | final key = keys.current; 174 | return decoder.whatsNext(key is String ? key : '', id: key is int ? key : null); 175 | } 176 | 177 | @override 178 | bool decodeBool() { 179 | final key = keys.current; 180 | return decoder.decodeBool(key is String ? key : '', id: key is int ? key : null); 181 | } 182 | 183 | @override 184 | bool? decodeBoolOrNull() { 185 | if (_done) return null; 186 | final key = keys.current; 187 | return decoder.decodeBoolOrNull(key is String ? key : '', id: key is int ? key : null); 188 | } 189 | 190 | @override 191 | int decodeInt() { 192 | final key = keys.current; 193 | return decoder.decodeInt(key is String ? key : '', id: key is int ? key : null); 194 | } 195 | 196 | @override 197 | int? decodeIntOrNull() { 198 | if (_done) return null; 199 | final key = keys.current; 200 | return decoder.decodeIntOrNull(key is String ? key : '', id: key is int ? key : null); 201 | } 202 | 203 | @override 204 | double decodeDouble() { 205 | final key = keys.current; 206 | return decoder.decodeDouble(key is String ? key : '', id: key is int ? key : null); 207 | } 208 | 209 | @override 210 | double? decodeDoubleOrNull() { 211 | if (_done) return null; 212 | final key = keys.current; 213 | return decoder.decodeDoubleOrNull(key is String ? key : '', id: key is int ? key : null); 214 | } 215 | 216 | @override 217 | num decodeNum() { 218 | final key = keys.current; 219 | return decoder.decodeNum(key is String ? key : '', id: key is int ? key : null); 220 | } 221 | 222 | @override 223 | num? decodeNumOrNull() { 224 | if (_done) return null; 225 | final key = keys.current; 226 | return decoder.decodeNumOrNull(key is String ? key : '', id: key is int ? key : null); 227 | } 228 | 229 | @override 230 | String decodeString() { 231 | final key = keys.current; 232 | return decoder.decodeString(key is String ? key : '', id: key is int ? key : null); 233 | } 234 | 235 | @override 236 | String? decodeStringOrNull() { 237 | if (_done) return null; 238 | final key = keys.current; 239 | return decoder.decodeStringOrNull(key is String ? key : '', id: key is int ? key : null); 240 | } 241 | 242 | @override 243 | bool decodeIsNull() { 244 | if (_done) return false; 245 | final key = keys.current; 246 | return decoder.decodeIsNull(key is String ? key : '', id: key is int ? key : null); 247 | } 248 | 249 | @override 250 | T decodeObject({Decodable? using}) { 251 | final key = keys.current; 252 | return decoder.decodeObject(key is String ? key : '', id: key is int ? key : null, using: using); 253 | } 254 | 255 | @override 256 | T? decodeObjectOrNull({Decodable? using}) { 257 | if (_done) return null; 258 | final key = keys.current; 259 | return decoder.decodeObjectOrNull(key is String ? key : '', id: key is int ? key : null, using: using); 260 | } 261 | 262 | @override 263 | List decodeList({Decodable? using}) { 264 | final key = keys.current; 265 | return decoder.decodeList(key is String ? key : '', id: key is int ? key : null, using: using); 266 | } 267 | 268 | @override 269 | List? decodeListOrNull({Decodable? using}) { 270 | if (_done) return null; 271 | final key = keys.current; 272 | return decoder.decodeListOrNull(key is String ? key : '', id: key is int ? key : null, using: using); 273 | } 274 | 275 | @override 276 | Map decodeMap({Decodable? keyUsing, Decodable? valueUsing}) { 277 | final key = keys.current; 278 | return decoder.decodeMap(key is String ? key : '', 279 | id: key is int ? key : null, keyUsing: keyUsing, valueUsing: valueUsing); 280 | } 281 | 282 | @override 283 | Map? decodeMapOrNull({Decodable? keyUsing, Decodable? valueUsing}) { 284 | if (_done) return null; 285 | final key = keys.current; 286 | return decoder.decodeMapOrNull(key is String ? key : '', 287 | id: key is int ? key : null, keyUsing: keyUsing, valueUsing: valueUsing); 288 | } 289 | 290 | @override 291 | IteratedDecoder decodeIterated() { 292 | final key = keys.current; 293 | return decoder.decodeIterated(key is String ? key : '', id: key is int ? key : null); 294 | } 295 | 296 | @override 297 | KeyedDecoder decodeKeyed() { 298 | final key = keys.current; 299 | return decoder.decodeKeyed(key is String ? key : '', id: key is int ? key : null); 300 | } 301 | 302 | @override 303 | MappedDecoder decodeMapped() { 304 | final key = keys.current; 305 | return decoder.decodeMapped(key is String ? key : '', id: key is int ? key : null); 306 | } 307 | 308 | @override 309 | Object? nextKey() { 310 | if (keys.moveNext()) { 311 | return keys.current; 312 | } else { 313 | _done = true; 314 | return null; 315 | } 316 | } 317 | 318 | @override 319 | void skipCurrentValue() { 320 | // do nothing 321 | } 322 | 323 | @override 324 | void skipRemainingKeys() { 325 | while (keys.moveNext()) { 326 | // do nothing 327 | } 328 | } 329 | 330 | @override 331 | bool isHumanReadable() { 332 | return decoder.isHumanReadable(); 333 | } 334 | 335 | @override 336 | KeyedDecoder clone() { 337 | return CompatKeyedDecoder._(decoder); 338 | } 339 | 340 | @override 341 | Never expect(String expect) { 342 | final key = keys.current; 343 | return decoder.expect(key is String ? key : '', expect, id: key is int ? key : null); 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/src/extended/generics.dart: -------------------------------------------------------------------------------- 1 | /// This file contains interfaces and helper utilities to work with generic types. 2 | /// The interfaces follow the same pattern as the [Encodable], [Decodable] and [Codable] interfaces and are 3 | /// suffixed with a number indicating the number of type parameters. 4 | /// 5 | /// Generic (en/de/)codables can be combined with child (en/de/)codables for each type parameter through the [use] 6 | /// extension methods: 7 | /// 8 | /// ```dart 9 | /// // Generic codables can only encode / decode the base type. 10 | /// final Codable1> boxCodable = ...; 11 | /// // Combine with another codable to get a fully defined codable for the specific type. 12 | /// final Codable> boxUriCodable = boxCodable.use(Uri.codable); 13 | /// ``` 14 | library generic; 15 | 16 | import 'package:codable_dart/core.dart'; 17 | 18 | // ============================== 19 | // Generics with 1 type parameter 20 | // ============================== 21 | 22 | abstract interface class Encodable1 implements Encodable { 23 | @override 24 | void encode(T value, Encoder encoder, [Encodable? encodableA]); 25 | } 26 | 27 | /// Variant of [Decodable] that decodes a generic value of type [T] with one type parameter [A]. 28 | abstract interface class Decodable1 implements Decodable { 29 | /// Decodes a value of type [T] using the [decoder]. 30 | /// 31 | /// The implementation should first use [Decoder.whatsNext] to determine the type of the encoded data. 32 | /// Then it should use one of the typed [Decoder].decode...() methods to decode into its target type. 33 | /// If the returned [DecodingType] is not supported, the implementation can use [Decoder.expect] to throw a detailed error. 34 | /// 35 | /// The [decodableA] parameter should be used to decode nested values of type [A]. When it is `null` the 36 | /// implementation may choose to throw an error or use a fallback way of decoding values of type [A]. 37 | @override 38 | T decode(Decoder decoder, [Decodable? decodableA]); 39 | } 40 | 41 | /// Variant of [Codable] that can encode and decode a generic value of type [T] with one type parameter [A]. 42 | abstract class Codable1 implements Codable, Decodable1, Encodable1 { 43 | const Codable1(); 44 | } 45 | 46 | extension UseEncodable1 on Encodable1 { 47 | /// Combines a generic [Encodable1] of type T with an explicit [Encodable] for its type parameter A. 48 | /// 49 | /// This will pass [encodableA] to [Encodable1.encode] when encoding a value of type [T]. 50 | Encodable use([Encodable? encodableA]) => _UseEncodable1(this, encodableA); 51 | } 52 | 53 | extension UseDecodable1 on Decodable1 { 54 | /// Combines a generic [Decodable1] of type T with an explicit [Decodable] for its type parameter A. 55 | /// 56 | /// This will pass [decodableA] to [Decodable1.decode] when decoding a value of type [T]. 57 | Decodable use([Decodable? decodableA]) => _UseDecodable1(this, decodableA); 58 | } 59 | 60 | extension UseCodable1 on Codable1 { 61 | /// Combines a generic [Codable1] of type T with an explicit [Codable] for its type parameter A. 62 | /// 63 | /// This will pass [codableA] to [Codable1.decode] and [Codable1.encode] when encoding and decoding a value of type [T]. 64 | Codable use([Codable? codableA]) => _UseCodable1(this, codableA); 65 | 66 | /// Combines a generic [Codable1] of type T with an explicit [Decodable] for its type parameter A. 67 | /// 68 | /// This will pass [decodableA] to [Codable1.decode] when decoding a value of type [T]. 69 | Decodable useDecodable([Decodable? decodableA]) => _UseDecodable1(this, decodableA); 70 | 71 | /// Combines a generic [Codable1] of type T with an explicit [Encodable] for its type parameter A. 72 | /// 73 | /// This will pass [encodableA] to [Codable1.encode] when encoding a value of type [T]. 74 | Encodable useEncodable([Encodable? encodableA]) => _UseEncodable1(this, encodableA); 75 | } 76 | 77 | abstract interface class ComposedDecodable1 implements Decodable { 78 | /// Extracts the child [Decodable] for the type parameter [A] from the composed [Decodable] for type T. 79 | R extract(R Function(Decodable? decodableA) fn); 80 | } 81 | 82 | class _UseEncodable1 implements Encodable { 83 | const _UseEncodable1(this._encodable, this._encodableA); 84 | 85 | final Encodable1 _encodable; 86 | final Encodable? _encodableA; 87 | 88 | @override 89 | void encode(T value, Encoder encoder) { 90 | _encodable.encode(value, encoder, _encodableA); 91 | } 92 | } 93 | 94 | class _UseDecodable1 implements ComposedDecodable1 { 95 | const _UseDecodable1(this._decodable, this._decodableA); 96 | 97 | final Decodable1 _decodable; 98 | final Decodable? _decodableA; 99 | 100 | @override 101 | T decode(Decoder decoder) { 102 | return _decodable.decode(decoder, _decodableA); 103 | } 104 | 105 | @override 106 | R extract(R Function(Decodable? decodableA) fn) { 107 | return fn(_decodableA); 108 | } 109 | } 110 | 111 | class _UseCodable1 implements Codable, ComposedDecodable1 { 112 | const _UseCodable1(this._codable, this._codableA); 113 | 114 | final Codable1 _codable; 115 | final Codable? _codableA; 116 | 117 | @override 118 | T decode(Decoder decoder) { 119 | return _codable.decode(decoder, _codableA); 120 | } 121 | 122 | @override 123 | void encode(T value, Encoder encoder) { 124 | _codable.encode(value, encoder, _codableA); 125 | } 126 | 127 | @override 128 | R extract(R Function(Codable? codableA) fn) { 129 | return fn(_codableA); 130 | } 131 | } 132 | 133 | // =============================== 134 | // Generics with 2 type parameters 135 | // =============================== 136 | 137 | /// Variant of [Encodable] that encodes a generic value of type [T] with two type parameters [A] and [B]. 138 | abstract interface class Encodable2 implements Encodable { 139 | /// Encodes the [value] using the [encoder] and optional [Encodable]s for the type parameters [A] and [B]. 140 | /// 141 | /// The implementation should use one of the typed [Encoder].encode...() methods to encode the value. 142 | /// It is expected to call exactly one of the encoding methods a single time. Never more or less. 143 | /// 144 | /// The [encodableA] and [encodableB] parameters should be used to encode nested values of type [A] and [B]. 145 | /// When either is `null` the implementation may choose to throw an error or use a fallback way of encoding 146 | /// values either type. 147 | @override 148 | void encode(T value, Encoder encoder, [Encodable? encodableA, Encodable? encodableB]); 149 | } 150 | 151 | /// Variant of [Decodable] that decodes a generic value of type [T] with two type parameters [A] and [B]. 152 | abstract interface class Decodable2 implements Decodable { 153 | /// Decodes a value of type [T] using the [decoder]. 154 | /// 155 | /// The implementation should first use [Decoder.whatsNext] to determine the type of the encoded data. 156 | /// Then it should use one of the typed [Decoder].decode...() methods to decode into its target type. 157 | /// If the returned [DecodingType] is not supported, the implementation can use [Decoder.expect] to throw a detailed error. 158 | /// 159 | /// The [decodableA] and [decodableB] parameters should be used to decode nested values of type [A] and [B]. 160 | /// When either is `null` the implementation may choose to throw an error or use a fallback way of decoding 161 | /// values of either type. 162 | @override 163 | T decode(Decoder decoder, [Decodable? decodableA, Decodable? decodableB]); 164 | } 165 | 166 | /// Variant of [Codable] that can encode and decode a generic value of type [T] with two type parameters [A] and [B]. 167 | abstract class Codable2 implements Codable, Decodable2, Encodable2 { 168 | const Codable2(); 169 | } 170 | 171 | extension UseEncodable2 on Encodable2 { 172 | /// Combines a generic [Encodable2] of type T with explicit [Encodable]s for its type parameters A and B. 173 | /// 174 | /// This will pass [encodableA] and [encodableB] to [Encodable2.encode] when encoding a value of type [T]. 175 | Encodable use([Encodable? encodableA, Encodable? encodableB]) => 176 | _UseEncodable2(this, encodableA, encodableB); 177 | } 178 | 179 | extension UseDecodable2 on Decodable2 { 180 | /// Combines a generic [Decodable2] of type T with explicit [Decodable]s for its type parameters A and B. 181 | /// 182 | /// This will pass [decodableA] and [decodableB] to [Decodable2.decode] when decoding a value of type [T]. 183 | Decodable use([Decodable? decodableA, Decodable? decodableB]) => 184 | _UseDecodable2(this, decodableA, decodableB); 185 | } 186 | 187 | extension UseCodable2 on Codable2 { 188 | /// Combines a generic [Codable2] of type T with explicit [Codable]s for its type parameters A and B. 189 | /// 190 | /// This will pass [codableA] and [codableB] to [Codable2.decode] and [Codable2.encode] when encoding and decoding 191 | /// a value of type [T]. 192 | Codable use([Codable? codableA, Codable? codableB]) => _UseCodable2(this, codableA, codableB); 193 | 194 | /// Combines a generic [Codable2] of type T with an explicit [Decodable]s for its type parameters A and B. 195 | /// 196 | /// This will pass [decodableA] and [decodableB] to [Codable2.decode] when decoding a value of type [T]. 197 | Decodable useDecodable([Decodable? decodableA, Decodable? decodableB]) => 198 | _UseDecodable2(this, decodableA, decodableB); 199 | 200 | /// Combines a generic [Codable2] of type T with an explicit [Encodable]s for its type parameters A and B. 201 | /// 202 | /// This will pass [encodableA] and [encodableB] to [Codable2.encode] when encoding a value of type [T]. 203 | Encodable useEncodable([Encodable? encodableA, Encodable? encodableB]) => 204 | _UseEncodable2(this, encodableA, encodableB); 205 | } 206 | 207 | abstract interface class ComposedDecodable2 implements Decodable { 208 | /// Extracts the child [Decodable]s for the type parameters [A] and [B] from the composed [Decodable] 209 | /// for type T. 210 | R extract(R Function(Decodable? decodableA, Decodable? decodableB) fn); 211 | } 212 | 213 | class _UseEncodable2 implements Encodable { 214 | const _UseEncodable2(this._encodable, this.encodableA, this.encodableB); 215 | 216 | final Encodable2 _encodable; 217 | final Encodable? encodableA; 218 | final Encodable? encodableB; 219 | 220 | @override 221 | void encode(T value, Encoder encoder) { 222 | _encodable.encode(value, encoder, encodableA, encodableB); 223 | } 224 | } 225 | 226 | class _UseDecodable2 implements ComposedDecodable2 { 227 | const _UseDecodable2(this._decodable, this.decodableA, this.decodableB); 228 | 229 | final Decodable2 _decodable; 230 | final Decodable? decodableA; 231 | final Decodable? decodableB; 232 | 233 | @override 234 | T decode(Decoder decoder) { 235 | return _decodable.decode(decoder, decodableA, decodableB); 236 | } 237 | 238 | @override 239 | R extract(R Function(Decodable? decodableA, Decodable? decodableB) fn) { 240 | return fn(decodableA, decodableB); 241 | } 242 | } 243 | 244 | class _UseCodable2 implements Codable, ComposedDecodable2 { 245 | const _UseCodable2(this._codable, this.codableA, this.codableB); 246 | 247 | final Codable2 _codable; 248 | final Codable? codableA; 249 | final Codable? codableB; 250 | 251 | @override 252 | T decode(Decoder decoder) { 253 | return _codable.decode(decoder, codableA, codableB); 254 | } 255 | 256 | @override 257 | void encode(T value, Encoder encoder) { 258 | _codable.encode(value, encoder, codableA, codableB); 259 | } 260 | 261 | @override 262 | R extract(R Function(Codable? codableA, Codable? codableB) fn) { 263 | return fn(codableA, codableB); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /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 = discriminator.resolve(); 24 | return decodable.decode(decoder); 25 | } 26 | return decodeFallback(decoder); 27 | } 28 | } 29 | 30 | abstract mixin class SuperDecodable1 implements SuperDecodable, Decodable1 { 31 | @override 32 | T decodeFallback(Decoder decoder, [Decodable? decodableA]) { 33 | throw 'Fallback decode not implemented for $T.'; 34 | } 35 | 36 | @override 37 | T decode(Decoder decoder, [Decodable? decodableA]) { 38 | final discriminator = findDiscriminator(decoder); 39 | if (discriminator != null) { 40 | final decodable = discriminator.resolve1(decodableA); 41 | return decodable.decode(decoder); 42 | } 43 | return decodeFallback(decoder, decodableA); 44 | } 45 | } 46 | 47 | abstract mixin class SuperDecodable2 implements SuperDecodable, Decodable2 { 48 | @override 49 | T decodeFallback(Decoder decoder, [Decodable? decodableA, Decodable? decodableB]) { 50 | throw 'Fallback decode not implemented for $T.'; 51 | } 52 | 53 | @override 54 | T decode(Decoder decoder, [Decodable? decodableA, Decodable? decodableB]) { 55 | final discriminator = findDiscriminator(decoder); 56 | if (discriminator != null) { 57 | final decodable = discriminator.resolve2(decodableA, decodableB); 58 | return decodable.decode(decoder); 59 | } 60 | return decodeFallback(decoder, decodableA, decodableB); 61 | } 62 | } 63 | 64 | extension SuperDecodableExtension on SuperDecodable { 65 | Discriminator? findDiscriminator(Decoder decoder, [String? currentValue, List? customDiscriminators]) { 66 | final discriminators = [...this.discriminators, ...?customDiscriminators]; 67 | 68 | String? discriminatorValue = currentValue; 69 | if (discriminatorValue == null && discriminatorKey != null) { 70 | discriminatorValue = decoder.clone().decodeMapped().decodeStringOrNull(discriminatorKey!); 71 | } 72 | 73 | for (var d in discriminators) { 74 | final discriminator = d.canDecodable(decoder, discriminatorKey, discriminatorValue); 75 | if (discriminator == null) continue; 76 | return discriminator; 77 | } 78 | 79 | return null; 80 | } 81 | } 82 | 83 | abstract base class Discriminator { 84 | const Discriminator.base(this.value); 85 | 86 | factory Discriminator(Object? value, Decodable Function() resolve) = _Discriminator0; 87 | 88 | static Discriminator arg1(Object? value, Decodable Function(Decodable? d1) resolve) => 89 | _Discriminator1(value, resolve); 90 | static Discriminator arg1Bounded(Object? value, Function resolve) => _Discriminator1(value, resolve); 91 | 92 | static Discriminator arg2( 93 | Object? value, Decodable Function(Decodable? d1, Decodable? d2) resolve) => 94 | _Discriminator2(value, resolve); 95 | static Discriminator arg2Bounded(Object? value, Function resolve) => 96 | _Discriminator2(value, resolve); 97 | 98 | static List> chain( 99 | List> discriminators, Decodable Function(Discriminator d) resolve) { 100 | return [ 101 | for (final d in discriminators) _Discriminator0(d.value, () => resolve(d)), 102 | ]; 103 | } 104 | 105 | static List> chain1( 106 | List> discriminators, Decodable Function(Discriminator d, Decodable? d1) resolve) { 107 | return [ 108 | for (final d in discriminators) _Discriminator1(d.value, (Decodable? d1) => resolve(d, d1)), 109 | ]; 110 | } 111 | 112 | static List> chain2(List> discriminators, 113 | Decodable Function(Discriminator d, Decodable? d1, Decodable? d2) resolve) { 114 | return [ 115 | for (final d in discriminators) 116 | _Discriminator2( 117 | d.value, (Decodable? d1, Decodable? d2) => resolve(d, d1, d2)), 118 | ]; 119 | } 120 | 121 | final Object? value; 122 | } 123 | 124 | extension DiscriminatorApply on Discriminator { 125 | Decodable resolve() { 126 | return _resolve(null, null); 127 | } 128 | 129 | Decodable resolve1(Decodable? d1) { 130 | return _resolve(d1, null); 131 | } 132 | 133 | Decodable resolve2(Decodable? d1, Decodable? d2) { 134 | return _resolve(d1, d2); 135 | } 136 | 137 | Decodable _resolve(Decodable? d1, Decodable? d2) { 138 | Decodable? result; 139 | if (this case _Discriminator0 d) { 140 | result = d._resolve(); 141 | } else if (this case _Discriminator1 d) { 142 | result = d.resolve(d1); 143 | } else if (this case _Discriminator2 d) { 144 | result = d.resolve(d1, d2); 145 | } 146 | 147 | if (result is Decodable) { 148 | return result; 149 | } 150 | throw 'Cannot resolve discriminator to decode type $T. Got ${result.runtimeType}.'; 151 | } 152 | } 153 | 154 | final class _Discriminator0 extends Discriminator { 155 | const _Discriminator0(super.value, this._resolve) : super.base(); 156 | 157 | final Decodable Function() _resolve; 158 | } 159 | 160 | final class _Discriminator1 extends Discriminator { 161 | const _Discriminator1(super.value, this._resolve) : super.base(); 162 | 163 | final Function _resolve; 164 | 165 | Decodable resolve(Decodable? d1) { 166 | final satisfiesA = isBounded(); 167 | if (satisfiesA) { 168 | return _resolve(d1); 169 | } else { 170 | return _resolve<$A>(d1); 171 | } 172 | } 173 | } 174 | 175 | final class _Discriminator2 extends Discriminator { 176 | const _Discriminator2(super.value, this._resolve) : super.base(); 177 | 178 | final Function _resolve; 179 | 180 | Decodable resolve(Decodable? d1, Decodable? d2) { 181 | final satisfiesA = isBounded(); 182 | final satisfiesB = isBounded(); 183 | if (satisfiesA && satisfiesB) { 184 | return _resolve(d1, d2); 185 | } else if (satisfiesA) { 186 | return _resolve(d1, d2); 187 | } else if (satisfiesB) { 188 | return _resolve<$A, B>(d1, d2); 189 | } else { 190 | return _resolve<$A, $B>(d1, d2); 191 | } 192 | } 193 | } 194 | 195 | extension DiscriminatorExtension on Discriminator { 196 | Discriminator? canDecodable(Decoder decoder, String? currentKey, String? currentValue) { 197 | final discriminator = this; 198 | 199 | if (identical(discriminator.value, 'use_as_default')) { 200 | return discriminator; 201 | } 202 | 203 | if (discriminator.value is Function) { 204 | if (discriminator.value case bool Function(Decoder) fn) { 205 | if (fn(decoder.clone())) { 206 | return discriminator; 207 | } else { 208 | return null; 209 | } 210 | } else { 211 | throw AssertionError('Discriminator function must be of type "bool Function(Decoder)".'); 212 | } 213 | } 214 | 215 | if (currentValue == discriminator.value) { 216 | return discriminator; 217 | } 218 | 219 | return null; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /lib/src/formats/json.dart: -------------------------------------------------------------------------------- 1 | /// JSON reference implementation. 2 | /// 3 | /// A format that encodes models to a JSON string or bytes. 4 | /// 5 | /// To decrease the effort for the reference implementation, this is based largely 6 | /// on the `crimson` package from pub. A "real" implementation would probably be 7 | /// fully custom for optimal performance. 8 | library json; 9 | 10 | import 'dart:convert'; 11 | 12 | import 'package:codable_dart/core.dart'; 13 | import 'package:codable_dart/extended.dart'; 14 | import 'package:codable_dart/standard.dart'; 15 | import 'package:crimson/crimson.dart'; 16 | 17 | extension JsonDecodable on Decodable { 18 | T fromJson(String json) { 19 | return fromJsonBytes(utf8.encode(json)); 20 | } 21 | 22 | T fromJsonBytes(List bytes) { 23 | return JsonDecoder.decode(bytes, this); 24 | } 25 | } 26 | 27 | extension JsonEncodable on Encodable { 28 | String toJson(T value) { 29 | return utf8.decode(toJsonBytes(value)); 30 | } 31 | 32 | List toJsonBytes(T value) { 33 | return JsonEncoder.encode(value, using: this); 34 | } 35 | } 36 | 37 | extension JsonSelfEncodableSelf on T { 38 | String toJson() { 39 | return utf8.decode(toJsonBytes()); 40 | } 41 | 42 | List toJsonBytes() { 43 | return JsonEncoder.encode(this); 44 | } 45 | } 46 | 47 | class JsonDecoder implements Decoder, IteratedDecoder, KeyedDecoder { 48 | JsonDecoder._(this._reader); 49 | final Crimson _reader; 50 | 51 | static T decode(List value, Decodable decodable) { 52 | return decodable.decode(JsonDecoder._(Crimson(value))); 53 | } 54 | 55 | @override 56 | DecodingType whatsNext() { 57 | final type = _reader.whatIsNext(); 58 | return switch (type) { 59 | JsonType.nil => DecodingType.nil, 60 | JsonType.bool => DecodingType.bool, 61 | JsonType.number => DecodingType.num, 62 | JsonType.string => DecodingType.string, 63 | JsonType.object => DecodingType.keyed, 64 | JsonType.array => DecodingType.iterated, 65 | }; 66 | } 67 | 68 | @override 69 | bool decodeBool() { 70 | return _reader.read() as bool; 71 | } 72 | 73 | @override 74 | bool? decodeBoolOrNull() { 75 | return _reader.read() as bool?; 76 | } 77 | 78 | @override 79 | double decodeDouble() { 80 | return _reader.readDouble(); 81 | } 82 | 83 | @override 84 | double? decodeDoubleOrNull() { 85 | return _reader.readDoubleOrNull(); 86 | } 87 | 88 | @override 89 | int decodeInt() { 90 | return _reader.readInt(); 91 | } 92 | 93 | @override 94 | int? decodeIntOrNull() { 95 | return _reader.readIntOrNull(); 96 | } 97 | 98 | @override 99 | num decodeNum() { 100 | return _reader.readNum(); 101 | } 102 | 103 | @override 104 | num? decodeNumOrNull() { 105 | return _reader.readNumOrNull(); 106 | } 107 | 108 | @override 109 | String decodeString() { 110 | return _reader.readString(); 111 | } 112 | 113 | @override 114 | String? decodeStringOrNull() { 115 | return _reader.readStringOrNull(); 116 | } 117 | 118 | @override 119 | bool decodeIsNull() { 120 | return _reader.skipNull(); 121 | } 122 | 123 | @override 124 | T decodeObject({Decodable? using}) { 125 | if (using != null) { 126 | return using.decode(this); 127 | } else { 128 | return _reader.read() as T; 129 | } 130 | } 131 | 132 | @override 133 | T? decodeObjectOrNull({Decodable? using}) { 134 | if (_reader.skipNull()) return null; 135 | return decodeObject(using: using); 136 | } 137 | 138 | @override 139 | List decodeList({Decodable? using}) { 140 | return [ 141 | for (; _reader.iterArray();) decodeObject(using: using), 142 | ]; 143 | } 144 | 145 | @override 146 | List? decodeListOrNull({Decodable? using}) { 147 | if (_reader.skipNull()) return null; 148 | return decodeList(using: using); 149 | } 150 | 151 | @override 152 | Map decodeMap({Decodable? keyUsing, Decodable? valueUsing}) { 153 | return { 154 | for (String? key; (key = _reader.iterObject()) != null;) 155 | StandardDecoder.decode(key, using: keyUsing): decodeObject(using: valueUsing), 156 | }; 157 | } 158 | 159 | @override 160 | Map? decodeMapOrNull({Decodable? keyUsing, Decodable? valueUsing}) { 161 | if (_reader.skipNull()) return null; 162 | return decodeMap(keyUsing: keyUsing, valueUsing: valueUsing); 163 | } 164 | 165 | @override 166 | IteratedDecoder decodeIterated() { 167 | return this; 168 | } 169 | 170 | @override 171 | KeyedDecoder decodeKeyed() { 172 | return this; 173 | } 174 | 175 | @override 176 | MappedDecoder decodeMapped() { 177 | return CompatMappedDecoder.wrap(this); 178 | } 179 | 180 | @override 181 | bool nextItem() { 182 | return _reader.iterArray(); 183 | } 184 | 185 | @override 186 | Object? nextKey() { 187 | return _reader.iterObject(); 188 | } 189 | 190 | @override 191 | void skipCurrentItem() { 192 | return _reader.skip(); 193 | } 194 | 195 | @override 196 | void skipCurrentValue() { 197 | return _reader.skip(); 198 | } 199 | 200 | @override 201 | void skipRemainingKeys() { 202 | return _reader.skipPartialObject(); 203 | } 204 | 205 | @override 206 | void skipRemainingItems() { 207 | return _reader.skipPartialArray(); 208 | } 209 | 210 | @override 211 | bool isHumanReadable() { 212 | return true; 213 | } 214 | 215 | @override 216 | JsonDecoder clone() { 217 | return JsonDecoder._(Crimson(_reader.buffer, _reader.offset)); 218 | } 219 | 220 | @override 221 | Never expect(String expected) { 222 | throw CodableException.unexpectedType( 223 | expected: expected, 224 | actual: _reader.whatIsNext().name, 225 | data: _reader.buffer, 226 | offset: _reader.offset, 227 | ); 228 | } 229 | } 230 | 231 | class JsonEncoder implements Encoder, IteratedEncoder { 232 | JsonEncoder._(this._writer) { 233 | _keyed = JsonKeyedEncoder._(_writer, this); 234 | } 235 | 236 | final CrimsonWriter _writer; 237 | late final JsonKeyedEncoder _keyed; 238 | 239 | static List encode(T value, {Encodable? using}) { 240 | var encoder = JsonEncoder._(CrimsonWriter()); 241 | encoder.encodeObject(value, using: using); 242 | return encoder._writer.toBytes(); 243 | } 244 | 245 | @override 246 | void encodeBool(bool value) { 247 | _writer.writeBool(value); 248 | } 249 | 250 | @override 251 | void encodeBoolOrNull(bool? value) { 252 | if (value == null) { 253 | _writer.writeNull(); 254 | } else { 255 | _writer.writeBool(value); 256 | } 257 | } 258 | 259 | @override 260 | void encodeInt(int value) { 261 | _writer.writeNum(value); 262 | } 263 | 264 | @override 265 | void encodeIntOrNull(int? value) { 266 | if (value == null) { 267 | _writer.writeNull(); 268 | } else { 269 | _writer.writeNum(value); 270 | } 271 | } 272 | 273 | @override 274 | void encodeDouble(double value) { 275 | _writer.writeNum(value); 276 | } 277 | 278 | @override 279 | void encodeDoubleOrNull(double? value) { 280 | if (value == null) { 281 | _writer.writeNull(); 282 | } else { 283 | _writer.writeNum(value); 284 | } 285 | } 286 | 287 | @override 288 | void encodeNum(num value) { 289 | _writer.writeNum(value); 290 | } 291 | 292 | @override 293 | void encodeNumOrNull(num? value) { 294 | if (value == null) { 295 | _writer.writeNull(); 296 | } else { 297 | _writer.writeNum(value); 298 | } 299 | } 300 | 301 | @override 302 | void encodeString(String value) { 303 | _writer.writeString(value); 304 | } 305 | 306 | @override 307 | void encodeStringOrNull(String? value) { 308 | if (value == null) { 309 | _writer.writeNull(); 310 | } else { 311 | _writer.writeString(value); 312 | } 313 | } 314 | 315 | @override 316 | void encodeNull() { 317 | _writer.writeNull(); 318 | } 319 | 320 | @override 321 | bool canEncodeCustom() { 322 | return false; 323 | } 324 | 325 | @override 326 | void encodeObject(T value, {Encodable? using}) { 327 | if (using != null) { 328 | using.encode(value, this); 329 | } else if (value is SelfEncodable) { 330 | value.encode(this); 331 | } else { 332 | _writer.write(value); 333 | } 334 | } 335 | 336 | @override 337 | void encodeObjectOrNull(T? value, {Encodable? using}) { 338 | if (value == null) { 339 | _writer.writeNull(); 340 | } else { 341 | encodeObject(value, using: using); 342 | } 343 | } 344 | 345 | @override 346 | void encodeIterable(Iterable value, {Encodable? using}) { 347 | _writer.writeArrayStart(); 348 | for (final e in value) { 349 | encodeObject(e, using: using); 350 | } 351 | _writer.writeArrayEnd(); 352 | } 353 | 354 | @override 355 | void encodeIterableOrNull(Iterable? value, {Encodable? using}) { 356 | if (value == null) { 357 | _writer.writeNull(); 358 | } else { 359 | encodeIterable(value, using: using); 360 | } 361 | } 362 | 363 | @override 364 | void encodeMap(Map value, {Encodable? keyUsing, Encodable? valueUsing}) { 365 | _writer.writeObjectStart(); 366 | for (final key in value.keys) { 367 | final v = value[key] as V; 368 | _writer.writeObjectKey(StandardEncoder.encode(key, using: keyUsing) as String); 369 | encodeObject(v, using: valueUsing); 370 | } 371 | _writer.writeObjectEnd(); 372 | } 373 | 374 | @override 375 | void encodeMapOrNull(Map? value, {Encodable? keyUsing, Encodable? valueUsing}) { 376 | if (value == null) { 377 | _writer.writeNull(); 378 | } else { 379 | encodeMap(value, keyUsing: keyUsing, valueUsing: valueUsing); 380 | } 381 | } 382 | 383 | @override 384 | IteratedEncoder encodeIterated() { 385 | _writer.writeArrayStart(); 386 | return this; 387 | } 388 | 389 | @override 390 | KeyedEncoder encodeKeyed() { 391 | _writer.writeObjectStart(); 392 | return _keyed; 393 | } 394 | 395 | @override 396 | void end() { 397 | _writer.writeArrayEnd(); 398 | } 399 | 400 | @override 401 | bool isHumanReadable() { 402 | return true; 403 | } 404 | } 405 | 406 | class JsonKeyedEncoder implements KeyedEncoder { 407 | JsonKeyedEncoder._(this._writer, this._parent); 408 | 409 | final CrimsonWriter _writer; 410 | final JsonEncoder _parent; 411 | 412 | @override 413 | void encodeBool(String key, bool value, {int? id}) { 414 | _writer.writeObjectKey(key); 415 | _writer.writeBool(value); 416 | } 417 | 418 | @override 419 | void encodeBoolOrNull(String key, bool? value, {int? id}) { 420 | _writer.writeObjectKey(key); 421 | _parent.encodeBoolOrNull(value); 422 | } 423 | 424 | @override 425 | void encodeInt(String key, int value, {int? id}) { 426 | _writer.writeObjectKey(key); 427 | _writer.writeNum(value); 428 | } 429 | 430 | @override 431 | void encodeIntOrNull(String key, int? value, {int? id}) { 432 | _writer.writeObjectKey(key); 433 | _parent.encodeIntOrNull(value); 434 | } 435 | 436 | @override 437 | void encodeDouble(String key, double value, {int? id}) { 438 | _writer.writeObjectKey(key); 439 | _writer.writeNum(value); 440 | } 441 | 442 | @override 443 | void encodeDoubleOrNull(String key, double? value, {int? id}) { 444 | _writer.writeObjectKey(key); 445 | _parent.encodeDoubleOrNull(value); 446 | } 447 | 448 | @override 449 | void encodeNum(String key, num value, {int? id}) { 450 | _writer.writeObjectKey(key); 451 | _writer.writeNum(value); 452 | } 453 | 454 | @override 455 | void encodeNumOrNull(String key, num? value, {int? id}) { 456 | _writer.writeObjectKey(key); 457 | _parent.encodeNumOrNull(value); 458 | } 459 | 460 | @override 461 | void encodeString(String key, String value, {int? id}) { 462 | _writer.writeObjectKey(key); 463 | _writer.writeString(value); 464 | } 465 | 466 | @override 467 | void encodeStringOrNull(String key, String? value, {int? id}) { 468 | _writer.writeObjectKey(key); 469 | _parent.encodeStringOrNull(value); 470 | } 471 | 472 | @override 473 | void encodeNull(String key, {int? id}) { 474 | _writer.writeObjectKey(key); 475 | _writer.writeNull(); 476 | } 477 | 478 | @override 479 | bool canEncodeCustom() { 480 | return false; 481 | } 482 | 483 | @override 484 | void encodeObject(String key, T value, {int? id, Encodable? using}) { 485 | _writer.writeObjectKey(key); 486 | _parent.encodeObject(value, using: using); 487 | } 488 | 489 | @override 490 | void encodeObjectOrNull(String key, T? value, {int? id, Encodable? using}) { 491 | _writer.writeObjectKey(key); 492 | _parent.encodeObjectOrNull(value, using: using); 493 | } 494 | 495 | @override 496 | void encodeIterable(String key, Iterable value, {int? id, Encodable? using}) { 497 | _writer.writeObjectKey(key); 498 | _parent.encodeIterable(value, using: using); 499 | } 500 | 501 | @override 502 | void encodeIterableOrNull(String key, Iterable? value, {int? id, Encodable? using}) { 503 | _writer.writeObjectKey(key); 504 | _parent.encodeIterableOrNull(value, using: using); 505 | } 506 | 507 | @override 508 | void encodeMap(String key, Map value, {int? id, Encodable? keyUsing, Encodable? valueUsing}) { 509 | _writer.writeObjectKey(key); 510 | _parent.encodeMap(value, keyUsing: keyUsing, valueUsing: valueUsing); 511 | } 512 | 513 | @override 514 | void encodeMapOrNull(String key, Map? value, 515 | {int? id, Encodable? keyUsing, Encodable? valueUsing}) { 516 | _writer.writeObjectKey(key); 517 | _parent.encodeMapOrNull(value, keyUsing: keyUsing, valueUsing: valueUsing); 518 | } 519 | 520 | @override 521 | IteratedEncoder encodeIterated(String key, {int? id}) { 522 | _writer.writeObjectKey(key); 523 | _writer.writeArrayStart(); 524 | return _parent; 525 | } 526 | 527 | @override 528 | KeyedEncoder encodeKeyed(String key, {int? id}) { 529 | _writer.writeObjectKey(key); 530 | _writer.writeObjectStart(); 531 | return this; 532 | } 533 | 534 | @override 535 | void end() { 536 | _writer.writeObjectEnd(); 537 | } 538 | 539 | @override 540 | bool isHumanReadable() { 541 | return true; 542 | } 543 | } 544 | -------------------------------------------------------------------------------- /lib/standard.dart: -------------------------------------------------------------------------------- 1 | export 'src/codec/standard.dart'; 2 | export 'src/formats/standard.dart'; 3 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: codable_dart 2 | description: A codable protocol for Dart. 3 | version: 1.0.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 | # TODO: Replace with own implementation 16 | crimson: ^0.3.1 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 | -------------------------------------------------------------------------------- /test/basic/basic_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:codable_dart/common.dart'; 4 | import 'package:codable_dart/json.dart'; 5 | import 'package:codable_dart/src/codec/csv.dart'; 6 | import 'package:codable_dart/src/codec/msgpack.dart'; 7 | import 'package:codable_dart/src/formats/msgpack.dart'; 8 | import 'package:codable_dart/standard.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | import 'model/person.dart'; 12 | import 'test_data.dart'; 13 | 14 | void main() { 15 | group("basic model", () { 16 | // Person to compare against. 17 | final expectedPerson = PersonRaw.fromMapRaw(personTestData); 18 | 19 | test("decodes from map", () { 20 | // Uses the fromMap extension on Decodable to decode the map. 21 | Person p = Person.codable.fromMap(personTestData); 22 | expect(p, equals(expectedPerson)); 23 | }); 24 | 25 | test("encodes to map", () { 26 | // Uses the toMap extension on SelfEncodable to encode the map. 27 | final Map encoded = expectedPerson.toMap(); 28 | expect(encoded, equals(personTestData)); 29 | }); 30 | 31 | test("decodes from json", () { 32 | // Uses the fromJson extension on Decodable to decode the json string. 33 | Person p = Person.codable.fromJson(personTestJson); 34 | expect(p, equals(expectedPerson)); 35 | }); 36 | 37 | test("encodes to json", () { 38 | // Uses the toJson extension on SelfEncodable to encode the json string. 39 | final String encoded = expectedPerson.toJson(); 40 | expect(encoded, equals(personTestJson)); 41 | }); 42 | 43 | test("decodes from json bytes", () { 44 | // Uses the fromJsonBytes extension on Decodable to decode the json bytes. 45 | Person p = Person.codable.fromJsonBytes(personTestJsonBytes); 46 | expect(p, equals(expectedPerson)); 47 | }); 48 | 49 | test("encodes to json bytes", () { 50 | // Uses the toJsonBytes extension on SelfEncodable to encode the json bytes. 51 | final List encoded = expectedPerson.toJsonBytes(); 52 | expect(encoded, equals(personTestJsonBytes)); 53 | }); 54 | 55 | test('decodes from msgpack bytes', () { 56 | // Uses the fromMsgPackBytes extension on Decodable to decode the msgpack bytes. 57 | Person p = Person.codable.fromMsgPack(personTestMsgpackBytes); 58 | expect(p, equals(expectedPerson)); 59 | }); 60 | 61 | test('encodes to msgpack bytes', () { 62 | // Uses the toMsgPackBytes extension on SelfEncodable to encode the msgpack bytes. 63 | final List encoded = expectedPerson.toMsgPack(); 64 | expect(encoded, equals(personTestMsgpackBytes)); 65 | }); 66 | 67 | group("using codec", () { 68 | test("decodes from map", () { 69 | // Uses the standard codec to decode the person from a map. 70 | Person p = Person.codable.codec.decode(personTestData); 71 | expect(p, equals(expectedPerson)); 72 | }); 73 | 74 | test("encodes to map", () { 75 | // Uses the standard codec to encode the person, and casts to a map. 76 | final Map encoded = Person.codable.codec.encode(expectedPerson) as Map; 77 | expect(encoded, equals(personTestData)); 78 | }); 79 | 80 | test("decodes from json", () { 81 | // Uses the json codec to decode the person from a String. 82 | Person p = Person.codable.codec.fuse(json).decode(personTestJson); 83 | expect(p, equals(expectedPerson)); 84 | }); 85 | 86 | test("encodes to json", () { 87 | // Uses the json codec to encode the person to a String. 88 | final String encoded = Person.codable.codec.fuse(json).encode(expectedPerson); 89 | expect(encoded, equals(personTestJson)); 90 | }); 91 | 92 | test("decodes from json bytes", () { 93 | // Uses the json codec to decode the person from bytes. 94 | Person p = Person.codable.codec.fuse(json).fuse(utf8).decode(personTestJsonBytes); 95 | expect(p, equals(expectedPerson)); 96 | }); 97 | 98 | test("encodes to json bytes", () { 99 | // Uses the json codec to encode the person to bytes. 100 | final List encoded = Person.codable.codec.fuse(json).fuse(utf8).encode(expectedPerson); 101 | expect(encoded, equals(personTestJsonBytes)); 102 | }); 103 | 104 | test('decodes from msgpack bytes', () { 105 | // Uses the msgpack codec to decode the Person from bytes. 106 | Person p = Person.codable.codec.fuse(msgPack).decode(personTestMsgpackBytes); 107 | expect(p, equals(expectedPerson)); 108 | }); 109 | 110 | test('encodes to msgpack bytes', () { 111 | // Uses the msgpack codec to encode the Person to bytes. 112 | final List encoded = Person.codable.codec.fuse(msgPack).encode(expectedPerson); 113 | expect(encoded, equals(personTestMsgpackBytes)); 114 | }); 115 | 116 | }); 117 | }); 118 | } 119 | -------------------------------------------------------------------------------- /test/basic/model/person.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:codable_dart/core.dart'; 4 | 5 | class Person with PersonRaw implements SelfEncodable { 6 | Person(this.name, this.age, this.height, this.isDeveloper, this.parent, this.hobbies, this.friends); 7 | 8 | static const Codable codable = PersonCodable(); 9 | 10 | final String name; 11 | final int age; 12 | final double height; 13 | final bool isDeveloper; 14 | final Person? parent; 15 | final List hobbies; 16 | final List friends; 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 | height == other.height && 26 | isDeveloper == other.isDeveloper && 27 | parent == other.parent && 28 | hobbies.indexed.every((e) => other.hobbies[e.$1] == e.$2) && 29 | friends.indexed.every((e) => other.friends[e.$1] == e.$2); 30 | 31 | @override 32 | int get hashCode => Object.hash(name, age, height, isDeveloper, parent, hobbies, friends); 33 | 34 | @override 35 | String toString() { 36 | return 'Person(name: $name, age: $age, height: $height, isDeveloper: $isDeveloper, parent: $parent, hobbies: $hobbies, friends: $friends)'; 37 | } 38 | 39 | // ====== Codable Code ====== 40 | // Keep in mind that the below code could also easily be generated by macros or a code generator. 41 | 42 | @override 43 | void encode(Encoder encoder) { 44 | encoder.encodeKeyed() 45 | ..encodeString('name', name) 46 | ..encodeInt('age', age) 47 | ..encodeDouble('height', height) 48 | ..encodeBool('isDeveloper', isDeveloper) 49 | ..encodeObjectOrNull('parent', parent) 50 | ..encodeIterable('hobbies', hobbies) 51 | ..encodeIterable('friends', friends) 52 | ..end(); 53 | } 54 | } 55 | 56 | /// Codable implementation for [Person]. 57 | /// 58 | /// This extends the [SelfCodable] class for a default implementation of [encode] and 59 | /// implements the [decode] method. 60 | class PersonCodable extends SelfCodable { 61 | const PersonCodable(); 62 | 63 | @override 64 | Person decode(Decoder decoder) { 65 | return switch (decoder.whatsNext()) { 66 | // If the format prefers mapped decoding, use mapped decoding. 67 | DecodingType.mapped || DecodingType.map => decodeMapped(decoder.decodeMapped()), 68 | // If the format prefers keyed decoding or is non-self describing, use keyed decoding. 69 | DecodingType.keyed || DecodingType.unknown => decodeKeyed(decoder.decodeKeyed()), 70 | _ => decoder.expect('mapped or keyed'), 71 | }; 72 | } 73 | 74 | Person decodeKeyed(KeyedDecoder keyed) { 75 | late String name; 76 | late int age; 77 | late double height; 78 | late bool isDeveloper; 79 | Person? parent; 80 | late List hobbies; 81 | late List friends; 82 | 83 | for (Object? key; (key = keyed.nextKey()) != null;) { 84 | switch (key) { 85 | case 'name': 86 | name = keyed.decodeString(); 87 | case 'age': 88 | age = keyed.decodeInt(); 89 | case 'height': 90 | height = keyed.decodeDouble(); 91 | case 'isDeveloper': 92 | isDeveloper = keyed.decodeBool(); 93 | case 'parent': 94 | parent = keyed.decodeObjectOrNull(using: Person.codable); 95 | case 'hobbies': 96 | hobbies = keyed.decodeList(); 97 | case 'friends': 98 | friends = keyed.decodeList(using: Person.codable); 99 | default: 100 | keyed.skipCurrentValue(); 101 | } 102 | } 103 | 104 | return Person(name, age, height, isDeveloper, parent, hobbies, friends); 105 | } 106 | 107 | Person decodeMapped(MappedDecoder mapped) { 108 | return Person( 109 | mapped.decodeString('name'), 110 | mapped.decodeInt('age'), 111 | mapped.decodeDouble('height'), 112 | mapped.decodeBool('isDeveloper'), 113 | mapped.decodeObjectOrNull('parent', using: Person.codable), 114 | mapped.decodeList('hobbies'), 115 | mapped.decodeList('friends', using: Person.codable), 116 | ); 117 | } 118 | } 119 | 120 | /// Baseline implementations for encoding and decoding a [Person] instance. 121 | /// 122 | /// This is how we usually encode and decode models in Dart (e.g. code generated by json_serializable). 123 | /// Its used as a baseline against checking performance and correctness of the codable implementation. 124 | mixin PersonRaw { 125 | static Person fromMapRaw(Map map) { 126 | return Person( 127 | map['name'] as String, 128 | (map['age'] as num).toInt(), 129 | (map['height'] as num).toDouble(), 130 | map['isDeveloper'] as bool, 131 | map['parent'] == null ? null : PersonRaw.fromMapRaw(map['parent'] as Map), 132 | (map['hobbies'] as List).cast(), 133 | (map['friends'] as List).map((e) => PersonRaw.fromMapRaw(e as Map)).toList(), 134 | ); 135 | } 136 | 137 | static Person fromJsonRaw(String json) { 138 | return fromMapRaw(jsonDecode(json) as Map); 139 | } 140 | 141 | static Person fromJsonBytesRaw(List json) { 142 | return fromMapRaw(jsonBytes.decode(json) as Map); 143 | } 144 | 145 | Map toMapRaw() { 146 | final value = this as Person; 147 | return { 148 | 'name': value.name, 149 | 'age': value.age, 150 | 'height': value.height, 151 | 'isDeveloper': value.isDeveloper, 152 | 'parent': value.parent?.toMapRaw(), 153 | 'hobbies': value.hobbies, 154 | 'friends': value.friends.map((e) => e.toMapRaw()).toList(), 155 | }; 156 | } 157 | 158 | String toJsonRaw() { 159 | return jsonEncode(toMapRaw()); 160 | } 161 | 162 | List toJsonBytesRaw() { 163 | return jsonBytes.encode(toMapRaw()); 164 | } 165 | } 166 | 167 | final jsonBytes = json.fuse(utf8); 168 | -------------------------------------------------------------------------------- /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 | // https://msgpack.org/ 52 | final personTestMsgpackBytes = base64Decode( 53 | 'h6RuYW1lq0FsaWNlIFNtaXRoo2FnZR6maGVpZ2h0y0AWZmZmZmZmq2lzRGV2ZWxvcGVyw6ZwYXJlbnSHpG5hbWWrQ2Fyb2wgU21pdGijYWdlN6ZoZWlnaHTLQBWZmZmZmZqraXNEZXZlbG9wZXLCpnBhcmVudMCnaG9iYmllc5KpZ2FyZGVuaW5np3JlYWRpbmenZnJpZW5kc5CnaG9iYmllc5OmY29kaW5npmhpa2luZ6hwYWludGluZ6dmcmllbmRzkoekbmFtZatCb2IgSm9obnNvbqNhZ2UgpmhlaWdodMtAF5mZmZmZmqtpc0RldmVsb3BlcsOmcGFyZW50h6RuYW1lrURhdmlkIEpvaG5zb26jYWdlPKZoZWlnaHQGq2lzRGV2ZWxvcGVywqZwYXJlbnTAp2hvYmJpZXORq3dvb2R3b3JraW5np2ZyaWVuZHOQp2hvYmJpZXOSpmdhbWluZ6djeWNsaW5np2ZyaWVuZHOQh6RuYW1lqUV2ZSBEYXZpc6NhZ2UcpmhlaWdodMtAFgAAAAAAAKtpc0RldmVsb3BlcsKmcGFyZW50wKdob2JiaWVzkqdkYW5jaW5nq3Bob3RvZ3JhcGh5p2ZyaWVuZHOQ'); 54 | -------------------------------------------------------------------------------- /test/benchmark/bench.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | 3 | void compare( 4 | String name, { 5 | required void Function() self, 6 | required void Function()? other, 7 | }) { 8 | test(name, () { 9 | print('== $name =='); 10 | bench('codable', self); 11 | if (other != null) { 12 | bench('baseline', other); 13 | } 14 | }); 15 | } 16 | 17 | void bench(String name, void Function() f, {int times = 10, bool sum = false}) { 18 | for (var i = 0; i < times / 2; i++) { 19 | f(); 20 | } 21 | final s = Stopwatch()..start(); 22 | for (var i = 0; i < times; i++) { 23 | f(); 24 | } 25 | s.stop(); 26 | var time = formatTime(s.elapsedMicroseconds ~/ (sum ? 1 : times)); 27 | print('$name: $time'); 28 | } 29 | 30 | String formatTime(int microseconds) { 31 | if (microseconds < 5000) { 32 | return '$microsecondsµs'; 33 | } else if (microseconds < 1000000) { 34 | return '${microseconds / 1000}ms'; 35 | } else { 36 | return '${microseconds / 1000000}s'; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/benchmark/performance_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:codable_dart/json.dart'; 4 | import 'package:codable_dart/standard.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | import '../basic/model/person.dart'; 8 | import '../basic/test_data.dart'; 9 | import 'bench.dart'; 10 | 11 | final personDeepData = { 12 | ...personTestData, 13 | 'parent': personTestData, 14 | 'friends': List.filled(100, personTestData), 15 | }; 16 | final personBenchData = { 17 | ...personDeepData, 18 | 'friends': List.filled(500, personDeepData), 19 | }; 20 | final personBenchJson = jsonEncode(personBenchData); 21 | final personBenchJsonBytes = utf8.encode(personBenchJson); 22 | 23 | void main() { 24 | Person p = PersonRaw.fromMapRaw(personBenchData); 25 | 26 | group('benchmark', tags: 'benchmark', () { 27 | compare( 28 | 'STANDARD DECODING (Map -> Person)', 29 | self: () => p = Person.codable.fromMap(personBenchData), 30 | other: () => p = PersonRaw.fromMapRaw(personBenchData), 31 | ); 32 | compare( 33 | 'STANDARD ENCODING (Person -> Map)', 34 | self: () => p.toMap(), 35 | other: () => p.toMapRaw(), 36 | ); 37 | 38 | print(''); 39 | 40 | compare( 41 | 'JSON STRING DECODING (String -> Person)', 42 | self: () => p = Person.codable.fromJson(personBenchJson), 43 | other: () => p = PersonRaw.fromJsonRaw(personBenchJson), 44 | ); 45 | compare( 46 | 'JSON STRING ENCODING (Person -> String)', 47 | self: () => p.toJson(), 48 | other: () => p.toJsonRaw(), 49 | ); 50 | 51 | print(''); 52 | 53 | compare( 54 | 'JSON BYTE DECODING (List -> Person)', 55 | self: () => p = Person.codable.fromJsonBytes(personBenchJsonBytes), 56 | other: () => p = PersonRaw.fromJsonBytesRaw(personBenchJsonBytes), 57 | ); 58 | compare( 59 | 'JSON BYTE ENCODING (Person -> List)', 60 | self: () => p.toJsonBytes(), 61 | other: () => p.toJsonBytesRaw(), 62 | ); 63 | 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/generics/basic/model/box.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | import 'package:codable_dart/extended.dart'; 3 | 4 | class Box implements SelfEncodable { 5 | Box(this.label, this.data); 6 | 7 | final String label; 8 | final T data; 9 | 10 | static const Codable1 codable = BoxCodable(); 11 | 12 | @override 13 | void encode(Encoder encoder, [Encodable? encodableT]) { 14 | encoder.encodeKeyed() 15 | ..encodeString('label', label) 16 | ..encodeObject('data', data, 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 BoxCodable extends Codable1, T> { 28 | const BoxCodable(); 29 | 30 | @override 31 | void encode(Box value, Encoder encoder, [Encodable? encodableA]) { 32 | value.encode(encoder, encodableA); 33 | } 34 | 35 | @override 36 | Box decode(Decoder decoder, [Decodable? decodableT]) { 37 | // For simplicity, we don't check the decoder.whatsNext() here. Don't do this for real implementations. 38 | final mapped = decoder.decodeMapped(); 39 | return Box( 40 | mapped.decodeString('label'), 41 | mapped.decodeObject('data', using: decodableT), 42 | ); 43 | } 44 | } 45 | 46 | extension BoxCodableExtension on Codable1 { 47 | // This is a convenience method for creating a BoxCodable with an explicit child codable. 48 | Codable> call<$A>([Codable<$A>? codableA]) => BoxCodable<$A>().use(codableA); 49 | } 50 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/hooks/hooks.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | 3 | import 'delegate.dart'; 4 | 5 | extension CodableHookExtension on Codable { 6 | /// Returns a [Codable] that applies the provided [Hook] when encoding and decoding [T]. 7 | Codable hook(Hook hook) => CodableHook(this, hook); 8 | } 9 | 10 | /// An object that can be used to modify the encoding and decoding behavior of a type of data format. 11 | abstract mixin class Hook { 12 | /// Called before decoding a value of type [T] using the [decoder] and [decodable]. 13 | /// 14 | /// The implementation may modify the decoding process by wrapping the [decoder] or [decodable], or 15 | /// by providing a custom decoding implementation. 16 | /// 17 | /// To forward to the original implementation, call `super.decode(decoder, decodable)`. 18 | T decode(Decoder decoder, Decodable decodable) => decodable.decode(decoder); 19 | 20 | /// Called before encoding a value of type [T] using the [encoder] and [encodable]. 21 | /// 22 | /// The implementation may modify the encoding process by wrapping the [encoder] or [encodable], or 23 | /// by providing a custom encoding implementation. 24 | /// 25 | /// To forward to the original implementation, call `super.encode(value, encoder, encodable)`. 26 | void encode(T value, Encoder encoder, Encodable encodable) => encodable.encode(value, encoder); 27 | } 28 | 29 | class CodableHook implements Codable { 30 | const CodableHook(this.codable, this.hook); 31 | 32 | final Codable codable; 33 | final Hook hook; 34 | 35 | @override 36 | T decode(Decoder decoder) { 37 | return hook.decode(decoder, codable); 38 | } 39 | 40 | @override 41 | void encode(T value, Encoder encoder) { 42 | hook.encode(value, encoder, codable); 43 | } 44 | } 45 | 46 | abstract mixin class ProxyHook implements Hook { 47 | @override 48 | T decode(Decoder decoder, Decodable decodable) { 49 | return decodable.decode(ProxyDecoder(decoder, this)); 50 | } 51 | 52 | String visitString(String value) => value; 53 | String? visitStringOrNull(String? value) => value; 54 | 55 | @override 56 | void encode(T value, Encoder encoder, Encodable encodable) { 57 | encoder.encodeObject(value, using: encodable); 58 | } 59 | } 60 | 61 | class ProxyDecoder extends RecursiveDelegatingDecoder { 62 | ProxyDecoder(super.wrapped, this.hook); 63 | 64 | final ProxyHook hook; 65 | 66 | @override 67 | String decodeString() { 68 | return hook.visitString(super.decodeString()); 69 | } 70 | 71 | @override 72 | String? decodeStringOrNull() { 73 | return hook.visitStringOrNull(super.decodeStringOrNull()); 74 | } 75 | 76 | @override 77 | Decoder clone() { 78 | return ProxyDecoder(delegate.clone(), hook); 79 | } 80 | 81 | @override 82 | ProxyDecoder wrap(Decoder decoder) { 83 | return ProxyDecoder(decoder, hook); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 const 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/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 | -------------------------------------------------------------------------------- /test/polymorphism/complex/complex_poly_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/common.dart'; 2 | import 'package:codable_dart/core.dart'; 3 | import 'package:codable_dart/extended.dart'; 4 | import 'package:codable_dart/standard.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | import 'model/box.dart'; 8 | 9 | final labelBoxMap = {'type': 'label', 'content': "some label"}; 10 | final anyBoxMap = { 11 | 'type': 'any', 12 | 'content': {'value': 'some value'} 13 | }; 14 | final numberBoxMap = {'type': 'number', 'content': 2.5}; 15 | final boxesMap = { 16 | 'type': 'boxes', 17 | 'content': [ 18 | {'value': 1}, 19 | {'value': 2}, 20 | {'value': 3} 21 | ] 22 | }; 23 | final metaBoxMap = {'type': 'meta', 'metadata': 2, 'content': 'test'}; 24 | final metaDataBoxMap = { 25 | 'type': 'meta', 26 | 'metadata': 2, 27 | 'content': {'value': 'test'} 28 | }; 29 | final higherOrderLabelBoxMap = {'type': 'higher_order', 'metadata': 'some metadata', 'content': labelBoxMap}; 30 | final higherOrderNumberBoxMap = {'type': 'higher_order', 'metadata': 100, 'content': numberBoxMap}; 31 | 32 | void main() { 33 | group('complex polymorphism', () { 34 | group("with reduced type parameter", () { 35 | test('decodes LabelBox as Box', () { 36 | final Codable> boxCodable = BoxCodable(); 37 | final Box box = boxCodable.fromMap(labelBoxMap); 38 | expect(box, isA()); 39 | expect(box.content, 'some label'); 40 | }); 41 | 42 | test('decodes LabelBox as Box', () { 43 | final Codable> boxCodable = BoxCodable(); 44 | final Box box = boxCodable.fromMap(labelBoxMap); 45 | expect(box, isA()); 46 | expect(box.content, 'some label'); 47 | }); 48 | 49 | test('decodes LabelBox not as Box', () { 50 | final Codable> boxCodable = BoxCodable(); 51 | expect( 52 | () { 53 | // ignore: unused_local_variable 54 | final Box box = boxCodable.fromMap(labelBoxMap); 55 | }, 56 | throwsA(isA().having( 57 | (e) => e.message, 58 | 'message', 59 | 'Failed to decode Box: Cannot resolve discriminator to decode type Box. Got LabelBoxCodable.', 60 | )), 61 | ); 62 | }); 63 | 64 | test('encodes LabelBox', () { 65 | final LabelBox box = LabelBox('some label'); 66 | final Map map = box.toMap(); 67 | expect(map, labelBoxMap); 68 | }); 69 | 70 | test('decodes AnyBox as Box', () { 71 | final Codable> boxCodable = BoxCodable(); 72 | final Box box = boxCodable.fromMap(anyBoxMap); 73 | expect(box, isA()); 74 | expect(box.content, {'value': 'some value'}); 75 | }); 76 | 77 | test('encodes AnyBox', () { 78 | final AnyBox box = AnyBox({'value': 'some value'}); 79 | final Map map = box.toMap(); 80 | expect(map, anyBoxMap); 81 | }); 82 | }); 83 | 84 | group("with bounded type parameter", () { 85 | test('decodes NumberBox as Box', () { 86 | final Codable> boxCodable = BoxCodable(); 87 | final Box box = boxCodable.fromMap(numberBoxMap); 88 | expect(box, isA()); 89 | expect(box.content, 2.5); 90 | }); 91 | test('decodes NumberBox as Box', () { 92 | final Codable> boxCodable = BoxCodable(); 93 | final Box box = boxCodable.fromMap(numberBoxMap); 94 | expect(box, isA()); 95 | expect(box.content, 2.5); 96 | }); 97 | test('decodes NumberBox as Box', () { 98 | final Codable> boxCodable = BoxCodable(); 99 | final Box box = boxCodable.fromMap(numberBoxMap); 100 | expect(box, isA>()); 101 | expect(box.content, 2.5); 102 | }); 103 | 104 | test('decodes NumberBox not as Box', () { 105 | final Codable> boxCodable = BoxCodable(); 106 | expect( 107 | () { 108 | // ignore: unused_local_variable 109 | final Box box = boxCodable.fromMap(numberBoxMap); 110 | }, 111 | throwsA(isA().having( 112 | (e) => e.message, 113 | 'message', 114 | 'Failed to decode Box: Cannot resolve discriminator to decode type Box. Got _UseDecodable1, num>.', 115 | )), 116 | ); 117 | }); 118 | 119 | test('encodes NumberBox', () { 120 | final NumberBox box = NumberBox(2.5); 121 | final Map map = box.toMap(); 122 | expect(map, numberBoxMap); 123 | }); 124 | }); 125 | 126 | group("with composed type parameter", () { 127 | test('decodes Boxes as Box', () { 128 | final Codable> boxCodable = BoxCodable(); 129 | final Box box = boxCodable.fromMap(boxesMap); 130 | expect(box, isA>()); 131 | expect(box.content, [ 132 | {'value': 1}, 133 | {'value': 2}, 134 | {'value': 3} 135 | ]); 136 | }); 137 | 138 | test('decodes Boxes as Box>', () { 139 | final Codable> boxCodable = BoxCodable(); 140 | final Box box = boxCodable.fromMap(boxesMap); 141 | expect(box, isA>()); 142 | expect(box.content, [ 143 | {'value': 1}, 144 | {'value': 2}, 145 | {'value': 3} 146 | ]); 147 | }); 148 | 149 | test('decodes Boxes as Box> with explicit child decode', () { 150 | final Codable>> boxCodable = BoxCodable>().use(Data.codable.list()); 151 | final Box> box = boxCodable.fromMap(boxesMap); 152 | expect(box, isA>()); 153 | expect(box.content, [ 154 | Data(1), 155 | Data(2), 156 | Data(3), 157 | ]); 158 | }); 159 | 160 | test('decodes Boxes not as Box> without explicit child decode', () { 161 | final Codable>> boxCodable = BoxCodable>().use(); 162 | 163 | expect( 164 | () { 165 | // ignore: unused_local_variable 166 | final Box> box = boxCodable.fromMap(boxesMap); 167 | }, 168 | throwsA(isA().having( 169 | (e) => e.message, 170 | 'message', 171 | 'Failed to decode Box>: Cannot resolve discriminator to decode type Box>. Got BoxesCodable.', 172 | )), 173 | ); 174 | }); 175 | 176 | test('encodes Boxes without explicit inner encodable', () { 177 | final Boxes box = Boxes([ 178 | {'value': 1}, 179 | {'value': 2}, 180 | {'value': 3} 181 | ]); 182 | final Map map = box.toMap(); 183 | expect(map, boxesMap); 184 | }); 185 | 186 | test('encodes Boxes with explicit inner encodable', () { 187 | final Boxes box = Boxes([ 188 | Data(1), 189 | Data(2), 190 | Data(3), 191 | ]); 192 | final Map map = box.use(Data.codable).toMap(); 193 | expect(map, boxesMap); 194 | }); 195 | 196 | test('encodes Boxes as Box> with explicit inner encodable', () { 197 | final Box> box = Boxes([ 198 | Data(1), 199 | Data(2), 200 | Data(3), 201 | ]); 202 | final Map map = box.use(Data.codable.list()).toMap(); 203 | expect(map, boxesMap); 204 | }); 205 | }); 206 | 207 | group("with additional type parameter", () { 208 | test('decodes MetaBox as Box', () { 209 | final Codable> boxCodable = BoxCodable(); 210 | final Box box = boxCodable.fromMap(metaBoxMap); 211 | expect(box, isA>()); 212 | expect((box as MetaBox).metadata, 2); 213 | expect(box.content, 'test'); 214 | }); 215 | 216 | test('decodes MetaBox as Box ', () { 217 | final Codable> boxCodable = BoxCodable(); 218 | final Box box = boxCodable.fromMap(metaBoxMap); 219 | expect(box, isA>()); 220 | expect((box as MetaBox).metadata, 2); 221 | expect(box.content, 'test'); 222 | }); 223 | 224 | test('decodes MetaBox as Box with explicit child decode', () { 225 | final Codable> boxCodable = BoxCodable().use(Data.codable); 226 | final Box box = boxCodable.fromMap(metaDataBoxMap); 227 | expect(box, isA>()); 228 | expect((box as MetaBox).metadata, 2); 229 | expect(box.content, Data('test')); 230 | }); 231 | 232 | test('encodes MetaBox with explicit inner encodable', () { 233 | final MetaBox box = MetaBox(2, Data('test')); 234 | final Map map = box.use(null, Data.codable).toMap(); 235 | expect(map, metaDataBoxMap); 236 | }); 237 | 238 | test('decodes HigherOrderBox as Box', () { 239 | final Codable> boxCodable = BoxCodable(); 240 | final Box box = boxCodable.fromMap(higherOrderLabelBoxMap); 241 | expect(box, isA>()); 242 | expect((box as HigherOrderBox).metadata, 'some metadata'); 243 | expect(box.content, isA()); 244 | expect(box.content.content, 'some label'); 245 | }); 246 | 247 | test('decodes HigherOrderBox as Box>', () { 248 | final Codable>> boxCodable = BoxCodable>(); 249 | final Box> box = boxCodable.fromMap(higherOrderLabelBoxMap); 250 | expect(box, isA>()); 251 | expect((box as HigherOrderBox).metadata, 'some metadata'); 252 | expect(box.content, isA()); 253 | expect(box.content.content, 'some label'); 254 | }); 255 | 256 | test('decodes HigherOrderBox as Box>', () { 257 | final Codable>> boxCodable = BoxCodable>(); 258 | final Box> box = boxCodable.fromMap(higherOrderLabelBoxMap); 259 | expect(box, isA>>()); 260 | expect((box as HigherOrderBox).metadata, 'some metadata'); 261 | expect(box.content, isA()); 262 | expect(box.content.content, 'some label'); 263 | }); 264 | 265 | test('decodes HigherOrderBox as Box', () { 266 | final Codable> boxCodable = BoxCodable(); 267 | final Box box = boxCodable.fromMap(higherOrderLabelBoxMap); 268 | expect(box, isA>()); 269 | expect((box as HigherOrderBox).metadata, 'some metadata'); 270 | expect(box.content, isA()); 271 | expect(box.content.content, 'some label'); 272 | }); 273 | 274 | test('decodes HigherOrderBox as Box', () { 275 | final Codable> boxCodable = BoxCodable(); 276 | final Box box = boxCodable.fromMap(higherOrderNumberBoxMap); 277 | expect(box, isA>()); 278 | expect((box as HigherOrderBox).metadata, 100); 279 | expect(box.content, isA()); 280 | expect(box.content.content, 2.5); 281 | }); 282 | }); 283 | }); 284 | } 285 | -------------------------------------------------------------------------------- /test/polymorphism/complex/model/box.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable_dart/core.dart'; 2 | import 'package:codable_dart/extended.dart'; 3 | 4 | abstract class Box implements SelfEncodable { 5 | const Box(this.content); 6 | 7 | final T content; 8 | 9 | @override 10 | void encode(Encoder encoder, [Encodable? encodableT]) { 11 | encoder.encodeKeyed() 12 | ..encodeObject('content', content, using: encodableT) 13 | ..end(); 14 | } 15 | } 16 | 17 | extension BoxEncodableExtension on Box { 18 | SelfEncodable use([Encodable? encodableT]) { 19 | return SelfEncodable.fromHandler((e) => encode(e, encodableT)); 20 | } 21 | } 22 | 23 | // For testing fixed type parameters. 24 | class LabelBox extends Box { 25 | const LabelBox(super.content); 26 | 27 | @override 28 | void encode(Encoder encoder, [_]) { 29 | encoder.encodeKeyed() 30 | ..encodeString('type', 'label') 31 | ..encodeString('content', content) 32 | ..end(); 33 | } 34 | } 35 | 36 | // For testing reduced type parameters. 37 | class AnyBox extends Box { 38 | const AnyBox(super.content); 39 | 40 | @override 41 | void encode(Encoder encoder, [_]) { 42 | encoder.encodeKeyed() 43 | ..encodeString('type', 'any') 44 | ..encodeObject('content', content) 45 | ..end(); 46 | } 47 | } 48 | 49 | // For testing bounded type parameters. 50 | class NumberBox extends Box { 51 | const NumberBox(super.content); 52 | 53 | @override 54 | void encode(Encoder encoder, [_]) { 55 | encoder.encodeKeyed() 56 | ..encodeString('type', 'number') 57 | ..encodeNum('content', content) 58 | ..end(); 59 | } 60 | } 61 | 62 | // For testing nested type parameters. 63 | class Boxes extends Box> { 64 | const Boxes(super.content); 65 | 66 | @override 67 | void encode(Encoder encoder, [Encodable>? encodableT, Encodable? encodableT2]) { 68 | final keyed = encoder.encodeKeyed(); 69 | keyed.encodeString('type', 'boxes'); 70 | if (encodableT2 == null && encodableT != null) { 71 | keyed.encodeObject('content', content, using: encodableT); 72 | } else { 73 | keyed.encodeIterable('content', content, using: encodableT2); 74 | } 75 | keyed.end(); 76 | } 77 | } 78 | 79 | extension BoxesEncodableExtension on Boxes { 80 | SelfEncodable use([Encodable? encodableT]) { 81 | return SelfEncodable.fromHandler((e) => encode(e, null, encodableT)); 82 | } 83 | } 84 | 85 | // For testing additional type parameters. 86 | class MetaBox extends Box { 87 | const MetaBox(this.metadata, super.content); 88 | 89 | final V metadata; 90 | 91 | @override 92 | void encode(Encoder encoder, [Encodable? encodableT, Encodable? encodableV]) { 93 | encoder.encodeKeyed() 94 | ..encodeString('type', 'meta') 95 | ..encodeObject('metadata', metadata, using: encodableV) 96 | ..encodeObject('content', content, using: encodableT) 97 | ..end(); 98 | } 99 | } 100 | 101 | extension MetaBoxEncodableExtension on MetaBox { 102 | SelfEncodable use([Encodable? encodableV, Encodable? encodableT]) { 103 | return SelfEncodable.fromHandler((e) => encode(e, encodableT, encodableV)); 104 | } 105 | } 106 | 107 | // For testing self-dependent type parameters. 108 | class HigherOrderBox> extends MetaBox { 109 | const HigherOrderBox(super.metadata, super.content); 110 | 111 | @override 112 | void encode(Encoder encoder, [Encodable? encodableB, Encodable? encodableT]) { 113 | encoder.encodeKeyed() 114 | ..encodeString('type', 'higher_order') 115 | ..encodeObject('metadata', metadata, using: encodableT) 116 | ..encodeObject('content', content, using: encodableB) 117 | ..end(); 118 | } 119 | } 120 | 121 | // Codable implementations 122 | // ======================= 123 | 124 | class BoxCodable with SuperDecodable1, T> implements Codable1, T> { 125 | const BoxCodable(); 126 | 127 | @override 128 | String get discriminatorKey => 'type'; 129 | 130 | @override 131 | List> get discriminators => [ 132 | Discriminator.arg1('label', <_>(_) { 133 | return LabelBoxCodable(); 134 | }), 135 | Discriminator.arg1('any', <_>(_) { 136 | return AnyBoxCodable(); 137 | }), 138 | Discriminator.arg1Bounded('number', (Decodable? decodableT) { 139 | return NumberBoxCodable().useDecodable(decodableT); 140 | }), 141 | Discriminator.arg1Bounded('boxes', (Decodable? decodableLT) { 142 | if (decodableLT case ComposedDecodable1 d) { 143 | return d.extract>((decodableT) => BoxesCodable().useDecodable(decodableT)); 144 | } else { 145 | return BoxesCodable(); 146 | } 147 | }), 148 | Discriminator.arg1('meta', (decodableT) { 149 | return MetaBoxCodable().useDecodable(null, decodableT); 150 | }), 151 | ...Discriminator.chain1(MetaBoxCodable().discriminators, (d, decodableT) { 152 | return d.resolve2, dynamic, T>(null, decodableT); 153 | }), 154 | ]; 155 | 156 | @override 157 | void encode(Box value, Encoder encoder, [Encodable? encodableT]) { 158 | value.encode(encoder, encodableT); 159 | } 160 | } 161 | 162 | class LabelBoxCodable implements Codable { 163 | @override 164 | LabelBox decode(Decoder decoder) { 165 | final mapped = decoder.decodeMapped(); 166 | return LabelBox( 167 | mapped.decodeString('content'), 168 | ); 169 | } 170 | 171 | @override 172 | void encode(LabelBox value, Encoder encoder) { 173 | value.encode(encoder); 174 | } 175 | } 176 | 177 | class AnyBoxCodable implements Codable { 178 | @override 179 | AnyBox decode(Decoder decoder, [Decodable? decodableT]) { 180 | final mapped = decoder.decodeMapped(); 181 | return AnyBox( 182 | mapped.decodeObject('content'), 183 | ); 184 | } 185 | 186 | @override 187 | void encode(AnyBox value, Encoder encoder) { 188 | value.encode(encoder); 189 | } 190 | } 191 | 192 | class NumberBoxCodable implements Codable1, T> { 193 | @override 194 | NumberBox decode(Decoder decoder, [Decodable? decodableT]) { 195 | final mapped = decoder.decodeMapped(); 196 | return NumberBox( 197 | mapped.decodeObject('content', using: decodableT), 198 | ); 199 | } 200 | 201 | @override 202 | void encode(NumberBox value, Encoder encoder, [Encodable? encodableT]) { 203 | value.encode(encoder, encodableT); 204 | } 205 | } 206 | 207 | class BoxesCodable implements Codable1, T> { 208 | @override 209 | Boxes decode(Decoder decoder, [Decodable? decodableT]) { 210 | final mapped = decoder.decodeMapped(); 211 | return Boxes( 212 | mapped.decodeList('content', using: decodableT), 213 | ); 214 | } 215 | 216 | @override 217 | void encode(Boxes value, Encoder encoder, [Encodable? encodableT]) { 218 | value.encode(encoder, null, encodableT); 219 | } 220 | } 221 | 222 | class MetaBoxCodable with SuperDecodable2, V, T> implements Codable2, V, T> { 223 | @override 224 | String? get discriminatorKey => 'type'; 225 | 226 | @override 227 | List> get discriminators => [ 228 | Discriminator.arg2Bounded( 229 | 'higher_order', 230 | >(Decodable? decodableT, Decodable? decodableB) { 231 | return HigherOrderBoxCodable().useDecodable(decodableT, decodableB); 232 | }, 233 | ), 234 | ]; 235 | 236 | @override 237 | MetaBox decodeFallback(Decoder decoder, [Decodable? decodableV, Decodable? decodableT]) { 238 | final mapped = decoder.decodeMapped(); 239 | return MetaBox( 240 | mapped.decodeObject('metadata', using: decodableV), 241 | mapped.decodeObject('content', using: decodableT), 242 | ); 243 | } 244 | 245 | @override 246 | void encode(MetaBox value, Encoder encoder, [Encodable? encodableV, Encodable? encodableT]) { 247 | value.encode(encoder, encodableT, encodableV); 248 | } 249 | } 250 | 251 | class HigherOrderBoxCodable> implements Codable2, T, B> { 252 | @override 253 | HigherOrderBox decode(Decoder decoder, [Decodable? decodableT, Decodable? decodableB]) { 254 | final mapped = decoder.decodeMapped(); 255 | return HigherOrderBox( 256 | mapped.decodeObject('metadata', using: decodableT), 257 | decodableB != null // 258 | ? mapped.decodeObject('content', using: decodableB) 259 | : mapped.decodeObject>('content', using: BoxCodable()) as B, 260 | ); 261 | } 262 | 263 | @override 264 | void encode(HigherOrderBox value, Encoder encoder, [Encodable? encodableT, Encodable? encodableB]) { 265 | value.encode(encoder, encodableB, encodableT); 266 | } 267 | } 268 | 269 | // For testing nested types. 270 | class Data implements SelfEncodable { 271 | const Data(this.value); 272 | 273 | final dynamic value; 274 | 275 | static const Codable codable = DataCodable(); 276 | 277 | @override 278 | void encode(Encoder encoder) { 279 | encoder.encodeKeyed() 280 | ..encodeObject('value', value) 281 | ..end(); 282 | } 283 | 284 | @override 285 | bool operator ==(Object other) => 286 | identical(this, other) || other is Data && runtimeType == other.runtimeType && value == other.value; 287 | 288 | @override 289 | int get hashCode => value.hashCode; 290 | 291 | @override 292 | String toString() => 'Data{value: $value}'; 293 | } 294 | 295 | class DataCodable extends SelfCodable { 296 | const DataCodable(); 297 | 298 | @override 299 | Data decode(Decoder decoder) { 300 | return Data(decoder.decodeMapped().decodeObject('value')); 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /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/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 | --------------------------------------------------------------------------------