diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..39b29d7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "codable", + "request": "launch", + "type": "dart", + "toolArgs": [ + "--enable-experiment=augmentations,enhanced-parts,macros,inference-update-4,num-shorthands,variance" + ] + } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index effe43c..3a76026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# Codable RFC + +## 1.0.1 + +- Added augment_test directory containing versions of examples as if + a builder was used to automatically generate augmentations as indicated by + proposed annotations. + ## 1.0.0 - Initial version. diff --git a/analysis_options.yaml b/analysis_options.yaml index f3edb69..dc25a46 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -3,3 +3,5 @@ include: package:lints/recommended.yaml analyzer: enable-experiment: - macros + - augmentations + diff --git a/pubspec.yaml b/pubspec.yaml index a85bff8..b38191d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ version: 1.0.0 repository: https://github.com/schultek/codable environment: - sdk: ^3.5.0 + sdk: ^3.7.0 topics: - codable diff --git a/test/augment_test/README.md b/test/augment_test/README.md new file mode 100644 index 0000000..5283626 --- /dev/null +++ b/test/augment_test/README.md @@ -0,0 +1,36 @@ + +Augmentation versions generally work except for: +------------------------------------------------------ + +generics/basic/model + box.dart // SEE NOTES in these files + box.codable.dart // augment versions can cause CRASHES in analyzer and cause weird errors in the analyzer + + +polymorphism/complex/model + box.dart // SEE NOTES in these files + box.codable.dart // augment versions can cause CRASHES in analyzer OR cause the weird analyzer errors + + + + + +-------- + +And currently (2/3/2025) the compiler is completely broken for the `augment` key word and nothing compiles (or even formats) + +I have filed + https://github.com/dart-lang/sdk/issues/60039 + +to detail the bug in the `augment` keyword + + +#Files used for sdk bug report issues: +---------------------------------------------------------- +test\augment_test\basic\model\superbasic_for_issue_bug_report.dart +stand alone code create for https://github.com/dart-lang/sdk/issues/60039 + + +test\augment_test\generics\basic\model\standalone_error.dart +Stand alone code created for https://github.com/dart-lang/sdk/issues/60040 + diff --git a/test/augment_test/basic/basic_test.dart b/test/augment_test/basic/basic_test.dart new file mode 100644 index 0000000..9796c49 --- /dev/null +++ b/test/augment_test/basic/basic_test.dart @@ -0,0 +1,62 @@ +import 'package:codable/json.dart'; +import 'package:codable/src/formats/msgpack.dart'; +import 'package:codable/standard.dart'; +import 'package:test/test.dart'; + +import 'model/person.dart'; +import 'test_data.dart'; + +void main() { + group("basic model", () { + // Person to compare against. + final expectedPerson = PersonRaw.fromMapRaw(personTestData); + + test("decodes from map", () { + // Uses the fromMap extension on Decodable to decode the map. + Person p = Person.codable.fromMap(personTestData); + expect(p, equals(expectedPerson)); + }); + + test("encodes to map", () { + // Uses the toMap extension on SelfEncodable to encode the map. + final Map encoded = expectedPerson.toMap(); + expect(encoded, equals(personTestData)); + }); + + test("decodes from json", () { + // Uses the fromJson extension on Decodable to decode the json string. + Person p = Person.codable.fromJson(personTestJson); + expect(p, equals(expectedPerson)); + }); + + test("encodes to json", () { + // Uses the toJson extension on SelfEncodable to encode the json string. + final String encoded = expectedPerson.toJson(); + expect(encoded, equals(personTestJson)); + }); + + test("decodes from json bytes", () { + // Uses the fromJsonBytes extension on Decodable to decode the json bytes. + Person p = Person.codable.fromJsonBytes(personTestJsonBytes); + expect(p, equals(expectedPerson)); + }); + + test("encodes to json bytes", () { + // Uses the toJsonBytes extension on SelfEncodable to encode the json bytes. + final List encoded = expectedPerson.toJsonBytes(); + expect(encoded, equals(personTestJsonBytes)); + }); + + test('decodes from msgpack bytes', () { + // Uses the fromMsgPackBytes extension on Decodable to decode the msgpack bytes. + Person p = Person.codable.fromMsgPack(personTestMsgpackBytes); + expect(p, equals(expectedPerson)); + }); + + test('encodes to msgpack bytes', () { + // Uses the toMsgPackBytes extension on SelfEncodable to encode the msgpack bytes. + final List encoded = expectedPerson.toMsgPack(); + expect(encoded, equals(personTestMsgpackBytes)); + }); + }); +} diff --git a/test/augment_test/basic/model/person.codable.dart b/test/augment_test/basic/model/person.codable.dart new file mode 100644 index 0000000..d9e7f03 --- /dev/null +++ b/test/augment_test/basic/model/person.codable.dart @@ -0,0 +1,118 @@ +// @dart = 3.8 +part of 'person.dart'; + +augment class Person implements SelfEncodable { + const Person(this.name, this.age, this.height, this.isDeveloper, this.parent, this.hobbies, this.friends); + + static const Codable codable = PersonCodable(); + + // ====== Codable Code ====== + // Keep in mind that the below code could also easily be generated by macros or a code generator. + + @override + void encode(Encoder encoder) { + encoder.encodeKeyed() + ..encodeString('name', name) + ..encodeInt('age', age) + ..encodeDouble('height', height) + ..encodeBool('isDeveloper', isDeveloper) + ..encodeObjectOrNull('parent', parent) + ..encodeIterable('hobbies', hobbies) + ..encodeIterable('friends', friends) + ..end(); + } +} + +// Equatable stuff +augment class Person { + @override + bool operator ==(Object other) => + identical(this, other) || + other is Person && + runtimeType == other.runtimeType && + name == other.name && + age == other.age && + height == other.height && + isDeveloper == other.isDeveloper && + parent == other.parent && + hobbies.indexed.every((e) => other.hobbies[e.$1] == e.$2) && + friends.indexed.every((e) => other.friends[e.$1] == e.$2); + + @override + int get hashCode => Object.hash(name, age, height, isDeveloper, parent, hobbies, friends); +} + +// toString stuff +augment class Person { + @override + String toString() { + return 'Person(name: $name, age: $age, height: $height, isDeveloper: $isDeveloper, parent: $parent, hobbies: $hobbies, friends: $friends)'; + } + +} + + + +/// Codable implementation for [Person]. +/// +/// This extends the [SelfCodable] class for a default implementation of [encode] and +/// implements the [decode] method. +class PersonCodable extends SelfCodable { + const PersonCodable(); + + @override + Person decode(Decoder decoder) { + return switch (decoder.whatsNext()) { + // If the format prefers mapped decoding, use mapped decoding. + DecodingType.mapped || DecodingType.map => decodeMapped(decoder.decodeMapped()), + // If the format prefers keyed decoding or is non-self describing, use keyed decoding. + DecodingType.keyed || DecodingType.unknown => decodeKeyed(decoder.decodeKeyed()), + _ => decoder.expect('mapped or keyed'), + }; + } + + Person decodeKeyed(KeyedDecoder keyed) { + late String name; + late int age; + late double height; + late bool isDeveloper; + Person? parent; + late List hobbies; + late List friends; + + for (Object? key; (key = keyed.nextKey()) != null;) { + switch (key) { + case 'name': + name = keyed.decodeString(); + case 'age': + age = keyed.decodeInt(); + case 'height': + height = keyed.decodeDouble(); + case 'isDeveloper': + isDeveloper = keyed.decodeBool(); + case 'parent': + parent = keyed.decodeObjectOrNull(using: Person.codable); + case 'hobbies': + hobbies = keyed.decodeList(); + case 'friends': + friends = keyed.decodeList(using: Person.codable); + default: + keyed.skipCurrentValue(); + } + } + + return Person(name, age, height, isDeveloper, parent, hobbies, friends); + } + + Person decodeMapped(MappedDecoder mapped) { + return Person( + mapped.decodeString('name'), + mapped.decodeInt('age'), + mapped.decodeDouble('height'), + mapped.decodeBool('isDeveloper'), + mapped.decodeObjectOrNull('parent', using: Person.codable), + mapped.decodeList('hobbies'), + mapped.decodeList('friends', using: Person.codable), + ); + } +} diff --git a/test/augment_test/basic/model/person.dart b/test/augment_test/basic/model/person.dart new file mode 100644 index 0000000..4ed674e --- /dev/null +++ b/test/augment_test/basic/model/person.dart @@ -0,0 +1,69 @@ +// @dart = 3.8 +import 'dart:convert'; + +import 'package:codable/core.dart'; + +part 'person.codable.dart'; + +//~@Codable(equatable:true,toString:true) +//! arguments could be used to trigger creation of equatable and toString() methods +class Person with PersonRaw { + + final String name; + final int age; + final double height; + final bool isDeveloper; + final Person? parent; + final List hobbies; + final List friends; + +} + +/// Baseline implementations for encoding and decoding a [Person] instance. +/// +/// This is how we usually encode and decode models in Dart (e.g. code generated by json_serializable). +/// Its used as a baseline against checking performance and correctness of the codable implementation. +mixin PersonRaw { + static Person fromMapRaw(Map map) { + return Person( + map['name'] as String, + (map['age'] as num).toInt(), + (map['height'] as num).toDouble(), + map['isDeveloper'] as bool, + map['parent'] == null ? null : PersonRaw.fromMapRaw(map['parent'] as Map), + (map['hobbies'] as List).cast(), + (map['friends'] as List).map((e) => PersonRaw.fromMapRaw(e as Map)).toList(), + ); + } + + static Person fromJsonRaw(String json) { + return fromMapRaw(jsonDecode(json) as Map); + } + + static Person fromJsonBytesRaw(List json) { + return fromMapRaw(jsonBytes.decode(json) as Map); + } + + Map toMapRaw() { + final value = this as Person; + return { + 'name': value.name, + 'age': value.age, + 'height': value.height, + 'isDeveloper': value.isDeveloper, + 'parent': value.parent?.toMapRaw(), + 'hobbies': value.hobbies, + 'friends': value.friends.map((e) => e.toMapRaw()).toList(), + }; + } + + String toJsonRaw() { + return jsonEncode(toMapRaw()); + } + + List toJsonBytesRaw() { + return jsonBytes.encode(toMapRaw()); + } +} + +final jsonBytes = json.fuse(utf8); diff --git a/test/augment_test/basic/model/superbasic_for_issue_bug_report.dart b/test/augment_test/basic/model/superbasic_for_issue_bug_report.dart new file mode 100644 index 0000000..c12931c --- /dev/null +++ b/test/augment_test/basic/model/superbasic_for_issue_bug_report.dart @@ -0,0 +1,20 @@ +// @dart = 3.7 + +class Person { + Person(this.name); + + final String name; +} + +augment class Person { + @override + String toString() { + return 'Person(name: $name)'; + } +} + +void main() { + var person = Person('John Doe'); + + print(person); +} diff --git a/test/augment_test/basic/test_data.dart b/test/augment_test/basic/test_data.dart new file mode 100644 index 0000000..2e53ddb --- /dev/null +++ b/test/augment_test/basic/test_data.dart @@ -0,0 +1,53 @@ +import 'dart:convert'; + +final personTestData = { + "name": "Alice Smith", + "age": 30, + "height": 5.6, + "isDeveloper": true, + "parent": { + "name": "Carol Smith", + "age": 55, + "height": 5.4, + "isDeveloper": false, + "parent": null, + "hobbies": ["gardening", "reading"], + "friends": [] + }, + "hobbies": ["coding", "hiking", "painting"], + "friends": [ + { + "name": "Bob Johnson", + "age": 32, + "height": 5.9, + "isDeveloper": true, + "parent": { + "name": "David Johnson", + "age": 60, + "height": 6.0, + "isDeveloper": false, + "parent": null, + "hobbies": ["woodworking"], + "friends": [] + }, + "hobbies": ["gaming", "cycling"], + "friends": [] + }, + { + "name": "Eve Davis", + "age": 28, + "height": 5.5, + "isDeveloper": false, + "parent": null, + "hobbies": ["dancing", "photography"], + "friends": [] + } + ] +}; + +final personTestJson = jsonEncode(personTestData); +final personTestJsonBytes = utf8.encode(personTestJson); + +// https://msgpack.org/ +final personTestMsgpackBytes = base64Decode( + 'h6RuYW1lq0FsaWNlIFNtaXRoo2FnZR6maGVpZ2h0y0AWZmZmZmZmq2lzRGV2ZWxvcGVyw6ZwYXJlbnSHpG5hbWWrQ2Fyb2wgU21pdGijYWdlN6ZoZWlnaHTLQBWZmZmZmZqraXNEZXZlbG9wZXLCpnBhcmVudMCnaG9iYmllc5KpZ2FyZGVuaW5np3JlYWRpbmenZnJpZW5kc5CnaG9iYmllc5OmY29kaW5npmhpa2luZ6hwYWludGluZ6dmcmllbmRzkoekbmFtZatCb2IgSm9obnNvbqNhZ2UgpmhlaWdodMtAF5mZmZmZmqtpc0RldmVsb3BlcsOmcGFyZW50h6RuYW1lrURhdmlkIEpvaG5zb26jYWdlPKZoZWlnaHQGq2lzRGV2ZWxvcGVywqZwYXJlbnTAp2hvYmJpZXORq3dvb2R3b3JraW5np2ZyaWVuZHOQp2hvYmJpZXOSpmdhbWluZ6djeWNsaW5np2ZyaWVuZHOQh6RuYW1lqUV2ZSBEYXZpc6NhZ2UcpmhlaWdodMtAFgAAAAAAAKtpc0RldmVsb3BlcsKmcGFyZW50wKdob2JiaWVzkqdkYW5jaW5nq3Bob3RvZ3JhcGh5p2ZyaWVuZHOQ'); diff --git a/test/augment_test/benchmark/bench.dart b/test/augment_test/benchmark/bench.dart new file mode 100644 index 0000000..659accb --- /dev/null +++ b/test/augment_test/benchmark/bench.dart @@ -0,0 +1,38 @@ +import 'package:test/test.dart'; + +void compare( + String name, { + required void Function() self, + required void Function()? other, +}) { + test(name, () { + print('== $name =='); + bench('codable', self); + if (other != null) { + bench('baseline', other); + } + }); +} + +void bench(String name, void Function() f, {int times = 10, bool sum = false}) { + for (var i = 0; i < times / 2; i++) { + f(); + } + final s = Stopwatch()..start(); + for (var i = 0; i < times; i++) { + f(); + } + s.stop(); + var time = formatTime(s.elapsedMicroseconds ~/ (sum ? 1 : times)); + print('$name: $time'); +} + +String formatTime(int microseconds) { + if (microseconds < 5000) { + return '$microsecondsµs'; + } else if (microseconds < 1000000) { + return '${microseconds / 1000}ms'; + } else { + return '${microseconds / 1000000}s'; + } +} diff --git a/test/augment_test/benchmark/performance_test.dart b/test/augment_test/benchmark/performance_test.dart new file mode 100644 index 0000000..5391ebb --- /dev/null +++ b/test/augment_test/benchmark/performance_test.dart @@ -0,0 +1,65 @@ +import 'dart:convert'; + +import 'package:codable/json.dart'; +import 'package:codable/standard.dart'; +import 'package:test/test.dart'; + +import '../basic/model/person.dart'; +import '../basic/test_data.dart'; +import 'bench.dart'; + +final personDeepData = { + ...personTestData, + 'parent': personTestData, + 'friends': List.filled(100, personTestData), +}; +final personBenchData = { + ...personDeepData, + 'friends': List.filled(500, personDeepData), +}; +final personBenchJson = jsonEncode(personBenchData); +final personBenchJsonBytes = utf8.encode(personBenchJson); + +void main() { + Person p = PersonRaw.fromMapRaw(personBenchData); + + group('benchmark', tags: 'benchmark', () { + compare( + 'STANDARD DECODING (Map -> Person)', + self: () => p = Person.codable.fromMap(personBenchData), + other: () => p = PersonRaw.fromMapRaw(personBenchData), + ); + compare( + 'STANDARD ENCODING (Person -> Map)', + self: () => p.toMap(), + other: () => p.toMapRaw(), + ); + + print(''); + + compare( + 'JSON STRING DECODING (String -> Person)', + self: () => p = Person.codable.fromJson(personBenchJson), + other: () => p = PersonRaw.fromJsonRaw(personBenchJson), + ); + compare( + 'JSON STRING ENCODING (Person -> String)', + self: () => p.toJson(), + other: () => p.toJsonRaw(), + ); + + print(''); + + compare( + 'JSON BYTE DECODING (List -> Person)', + self: () => p = Person.codable.fromJsonBytes(personBenchJsonBytes), + other: () => p = PersonRaw.fromJsonBytesRaw(personBenchJsonBytes), + ); + compare( + 'JSON BYTE ENCODING (Person -> List)', + self: () => p.toJsonBytes(), + other: () => p.toJsonBytesRaw(), + ); + + }); +} diff --git a/test/augment_test/collections/collections_test.dart b/test/augment_test/collections/collections_test.dart new file mode 100644 index 0000000..a44bd3e --- /dev/null +++ b/test/augment_test/collections/collections_test.dart @@ -0,0 +1,92 @@ +import 'package:codable/common.dart'; +import 'package:codable/core.dart'; +import 'package:codable/json.dart'; +import 'package:test/test.dart'; + +import '../basic/model/person.dart'; +import '../basic/test_data.dart'; + +void main() { + group("collections", () { + final List personList = [ + for (int i = 0; i < 10; i++) PersonRaw.fromMapRaw({...personTestData, 'name': 'Person $i'}), + ]; + final String personListJson = '[${personList.map((p) => p.toJsonRaw()).join(',')}]'; + + test("decodes as list", () { + // Get the codable for a list of persons. + final Codable> codable = Person.codable.list(); + // Use the fromJson extension method to decode the list. + List list = codable.fromJson(personListJson); + expect(list, equals(personList)); + }); + + test("encodes to list", () { + // Use the encode.toJson extension method to encode the list. + final encoded = personList.encode.toJson(); + expect(encoded, equals(personListJson)); + }); + + final Set personSet = personList.toSet(); + + test("decodes as set", () { + // Get the codable for a set of persons. + final Codable> codable = Person.codable.set(); + // Use the fromJson extension method to decode the set. + Set set = codable.fromJson(personListJson); + expect(set, equals(personSet)); + }); + + test("encodes to set", () { + // Use the encode.toJson extension method to encode the set. + final encoded = personSet.encode.toJson(); + expect(encoded, equals(personListJson)); + }); + + final Map personMap = { + for (final p in personList) p.name: p, + }; + final String personMapJson = '{${personMap.entries.map((e) { + return '"${e.key}":${e.value.toJsonRaw()}'; + }).join(',')}}'; + + test("decodes as map", () { + // Get the codable for a map of strings to persons. + final Codable> codable = Person.codable.map(); + // Use the fromJson extension method to decode the map. + Map map = codable.fromJson(personMapJson); + expect(map, equals(personMap)); + }); + + test("encodes to map", () { + // Use the encode.toJson extension method to encode the map. + final encoded = personMap.encode.toJson(); + expect(encoded, equals(personMapJson)); + }); + + final Map personUriMap = { + for (final p in personList) Uri.parse('example.com/person/${p.name}'): p, + }; + final String personUriMapJson = '{${personUriMap.entries.map((e) { + return '"${e.key}":${e.value.toJsonRaw()}'; + }).join(',')}}'; + + test("decodes as uri map", () { + // Construct the codable for a map of uris to persons. + // Provide the explicit codable for the key type. + final Codable> codable = Person.codable.map(UriCodable()); + // Use the fromJson method to decode the map. + Map map = codable.fromJson(personUriMapJson); + expect(map, equals(personUriMap)); + }); + + test("encodes to uri map", () { + // Construct the codable for a map of uris to persons. + // Provide the explicit codable for the key type. + final Codable> codable = Person.codable.map(UriCodable()); + // Use the toJson method to encode the map. + final encoded = codable.toJson(personUriMap); + expect(encoded, equals(personUriMapJson)); + }); + }); +} diff --git a/test/augment_test/csv/csv_test.dart b/test/augment_test/csv/csv_test.dart new file mode 100644 index 0000000..4bb56e2 --- /dev/null +++ b/test/augment_test/csv/csv_test.dart @@ -0,0 +1,43 @@ +import 'package:codable/common.dart'; +import 'package:codable/csv.dart'; +import 'package:codable/json.dart'; +import 'package:test/test.dart'; + +import 'model/measures.dart'; +import 'test_data.dart'; + +void main() { + group('csv', () { + // Since CSV always deals with rows of data, the fromCsv and toCsv methods deal directly + // with Lists of objects instead of single objects. + + test('decodes', () { + // Use the fromCsv extension method to decode the data. + List measures = Measures.codable.fromCsv(measuresCsv); + expect(measures, equals(measuresObjects)); + }); + + test('encodes', () { + // Use the toCsv extension method to encode the data. + final encoded = measuresObjects.toCsv(); + expect(encoded, equals(measuresCsv)); + }); + + // This shows how to easily switch between data formats given the same model implementation. + test('interop with json', () { + // Use the fromCsv extension method to decode the data from csv. + List measures = Measures.codable.fromCsv(measuresCsv); + // Use the encode.toJson extension method to encode the data to json. + final json = measures.encode.toJson(); + + expect(json, equals(measuresJson)); + + // Use the fromJson extension method to decode the data from json. + List measures2 = Measures.codable.list().fromJson(json); + // Use the toCsv extension method to encode the data to csv. + final csv = measures2.toCsv(); + + expect(csv, equals(measuresCsv)); + }); + }); +} diff --git a/test/augment_test/csv/model/measures.codable.dart b/test/augment_test/csv/model/measures.codable.dart new file mode 100644 index 0000000..4be1af4 --- /dev/null +++ b/test/augment_test/csv/model/measures.codable.dart @@ -0,0 +1,101 @@ +part of 'measures.dart'; + +augment class Measures implements SelfEncodable { + const Measures(this.id, this.name, this.age, this.isActive, this.signupDate, this.website); + + static const Codable codable = MeasuresCodable(); + + @override + void encode(Encoder encoder) { + final keyed = encoder.encodeKeyed(); + keyed.encodeString('id', id); + keyed.encodeStringOrNull('name', name); + keyed.encodeInt('age', age); + keyed.encodeBool('isActive', isActive); + keyed.encodeObjectOrNull('signupDate', signupDate, using: const DateTimeCodable()); + keyed.encodeObjectOrNull('website', website, using: const UriCodable()); + keyed.end(); + } +} + +// equatable augmentation +augment class Measures { + @override + bool operator ==(Object other) { + return identical(this, other) || + other is Measures && + runtimeType == other.runtimeType && + id == other.id && + name == other.name && + age == other.age && + isActive == other.isActive && + signupDate == other.signupDate && + website == other.website; + } + + @override + int get hashCode => Object.hash(id, name, age, isActive, signupDate, website); +} + +// toString augmentation +augment class Measures { + @override + String toString() { + return 'Measures{id: $id, name: $name, age: $age, isActive: $isActive, signupDate: $signupDate, website: $website}'; + } +} + + +class MeasuresCodable extends SelfCodable { + const MeasuresCodable(); + + @override + Measures decode(Decoder decoder) { + return switch (decoder.whatsNext()) { + DecodingType.mapped || DecodingType.map => decodeMapped(decoder.decodeMapped()), + DecodingType.keyed || DecodingType.unknown => decodeKeyed(decoder.decodeKeyed()), + _ => decoder.expect('keyed or mapped'), + }; + } + + Measures decodeKeyed(KeyedDecoder decoder) { + late String id; + late String? name; + late int age; + late bool isActive; + late DateTime? signupDate; + late Uri? website; + + for (Object? key; (key = decoder.nextKey()) != null;) { + switch (key) { + case 'id': + id = decoder.decodeString(); + case 'name': + name = decoder.decodeStringOrNull(); + case 'age': + age = decoder.decodeInt(); + case 'isActive': + isActive = decoder.decodeBool(); + case 'signupDate': + signupDate = decoder.decodeObjectOrNull(using: const DateTimeCodable()); + case 'website': + website = decoder.decodeObjectOrNull(using: const UriCodable()); + default: + decoder.skipCurrentValue(); + } + } + + return Measures(id, name, age, isActive, signupDate, website); + } + + Measures decodeMapped(MappedDecoder decoder) { + return Measures( + decoder.decodeString('id'), + decoder.decodeStringOrNull('name'), + decoder.decodeInt('age'), + decoder.decodeBool('isActive'), + decoder.decodeObjectOrNull('signupDate', using: const DateTimeCodable()), + decoder.decodeObjectOrNull('website', using: const UriCodable()), + ); + } +} diff --git a/test/augment_test/csv/model/measures.dart b/test/augment_test/csv/model/measures.dart new file mode 100644 index 0000000..0d0afd5 --- /dev/null +++ b/test/augment_test/csv/model/measures.dart @@ -0,0 +1,16 @@ +import 'package:codable/common.dart'; +import 'package:codable/core.dart'; + +part 'measures.codable.dart'; + +//~@Codable(equatable:true,toString:true) +//! arguments could be used to trigger creation of equatable and toString() methods +class Measures { + final String id; + final String? name; + final int age; + final bool isActive; + final DateTime? signupDate; + final Uri? website; +} + diff --git a/test/augment_test/csv/test_data.dart b/test/augment_test/csv/test_data.dart new file mode 100644 index 0000000..b4253c7 --- /dev/null +++ b/test/augment_test/csv/test_data.dart @@ -0,0 +1,65 @@ +import 'dart:convert'; + +import 'model/measures.dart'; + +final measuresCsv = ''' +id,name,age,isActive,signupDate,website +1,John Doe,25,true,2023-06-15T00:00:00.000,https://johndoe.com +2,Jane Smith,30,false,,https://janesmith.org +3,,45,true,2021-09-12T00:00:00.000,https://example.com +4,Alex Brown,29,false,2020-11-23T00:00:00.000, +5,Chris Johnson,34,true,2019-03-10T00:00:00.000,https://chrisjohnson.net +'''; + +final measuresObjects = [ + Measures('1', 'John Doe', 25, true, DateTime(2023, 6, 15), Uri.parse('https://johndoe.com')), + Measures('2', 'Jane Smith', 30, false, null, Uri.parse('https://janesmith.org')), + Measures('3', null, 45, true, DateTime(2021, 9, 12), Uri.parse('https://example.com')), + Measures('4', 'Alex Brown', 29, false, DateTime(2020, 11, 23), null), + Measures('5', 'Chris Johnson', 34, true, DateTime(2019, 3, 10), Uri.parse('https://chrisjohnson.net')), +]; + +final measuresData = [ + { + "id": "1", + "name": "John Doe", + "age": 25, + "isActive": true, + "signupDate": "2023-06-15T00:00:00.000", + "website": "https://johndoe.com" + }, + { + "id": "2", + "name": "Jane Smith", + "age": 30, + "isActive": false, + "signupDate": null, + "website": "https://janesmith.org" + }, + { + "id": "3", + "name": null, + "age": 45, + "isActive": true, + "signupDate": "2021-09-12T00:00:00.000", + "website": "https://example.com" + }, + { + "id": "4", + "name": "Alex Brown", + "age": 29, + "isActive": false, + "signupDate": "2020-11-23T00:00:00.000", + "website": null, + }, + { + "id": "5", + "name": "Chris Johnson", + "age": 34, + "isActive": true, + "signupDate": "2019-03-10T00:00:00.000", + "website": "https://chrisjohnson.net" + } +]; + +final measuresJson = jsonEncode(measuresData); diff --git a/test/augment_test/enum/enum_test.dart b/test/augment_test/enum/enum_test.dart new file mode 100644 index 0000000..417dce4 --- /dev/null +++ b/test/augment_test/enum/enum_test.dart @@ -0,0 +1,40 @@ +import 'package:codable/standard.dart'; +import 'package:test/test.dart'; + +import 'model/color.dart'; + +void main() { + group('enums', () { + test('decode from string', () { + final Color decoded = Color.codable.fromValue('green'); + expect(decoded, Color.green); + }); + + test('encode to string', () { + final Object? encoded = Color.green.toValue(); + expect(encoded, isA()); + expect(encoded, 'green'); + }); + + test('decode from null', () { + final Color decoded = Color.codable.fromValue(null); + expect(decoded, Color.none); + }); + + test('encode to null', () { + final Object? encoded = Color.none.toValue(); + expect(encoded, isNull); + }); + + test('decode from int', () { + final Color decoded = StandardDecoder.decode(1, using: Color.codable, isHumanReadable: false); + expect(decoded, Color.blue); + }); + + test('encode to int', () { + final Object? encoded = StandardEncoder.encode(Color.blue, using: Color.codable, isHumanReadable: false); + expect(encoded, isA()); + expect(encoded, 1); + }); + }); +} diff --git a/test/augment_test/enum/model/color.codable.dart b/test/augment_test/enum/model/color.codable.dart new file mode 100644 index 0000000..735abc9 --- /dev/null +++ b/test/augment_test/enum/model/color.codable.dart @@ -0,0 +1,73 @@ +part of 'color.dart'; + +/// Enums can define static [Codable]s and implement [SelfEncodable] just like normal classes. +augment enum Color implements SelfEncodable { + ; + + static const Codable codable = ColorCodable(); + + // This is a more elaborate implementation to showcase the flexibility of the codable protocol. + // You could also just have a fixed string or int encoding for all formats. + @override + void encode(Encoder encoder) { + if (encoder.isHumanReadable()) { + encoder.encodeStringOrNull(switch (this) { + Color.green => 'green', + Color.blue => 'blue', + Color.red => 'red', + Color.none => null, + }); + } else { + encoder.encodeIntOrNull(switch (this) { + Color.green => 0, + Color.blue => 1, + Color.red => 2, + Color.none => null, + }); + } + } + +} + +class ColorCodable extends SelfCodable { + const ColorCodable(); + + // This is a more elaborate implementation to showcase the flexibility of the codable protocol. + // You could also just have a fixed string or int decoding for all formats. + @override + Color decode(Decoder decoder) { + return switch (decoder.whatsNext()) { + // Enums (as any other class) may treat 'null' as a value or fallback to a default value. + DecodingType.nil => Color.none, + DecodingType.string => decodeString(decoder.decodeStringOrNull(), decoder), + DecodingType.num || DecodingType.int => decodeInt(decoder.decodeIntOrNull(), decoder), + DecodingType.unknown when decoder.isHumanReadable() => decodeString(decoder.decodeStringOrNull(), decoder), + DecodingType.unknown => decodeInt(decoder.decodeIntOrNull(), decoder), + _ => decoder.expect('Color as string or int'), + }; + } + + Color decodeString(String? value, Decoder decoder) { + return switch (value) { + 'green' => Color.green, + 'blue' => Color.blue, + 'red' => Color.red, + // Enums (as any other class) may treat 'null' as a value or fallback to a default value. + null => Color.none, + // Throw an error on any unknown value. We could also choose a default value here as well. + _ => decoder.expect('Color of green, blue, red or null'), + }; + } + + Color decodeInt(int? value, Decoder decoder) { + return switch (value) { + 0 => Color.green, + 1 => Color.blue, + 2 => Color.red, + // Enums (as any other class) may treat 'null' as a value or fallback to a default value. + null => Color.none, + // Throw an error on any unknown value. We could also choose a default value here as well. + _ => decoder.expect('Color of 0, 1, 2 or 3'), + }; + } +} diff --git a/test/augment_test/enum/model/color.dart b/test/augment_test/enum/model/color.dart new file mode 100644 index 0000000..53f591b --- /dev/null +++ b/test/augment_test/enum/model/color.dart @@ -0,0 +1,11 @@ +import 'package:codable/core.dart'; + +part 'color.codable.dart'; + +//~@Codable() +enum Color { + none, + green, + blue, + red; +} diff --git a/test/augment_test/error_handling/error_handling_test.dart b/test/augment_test/error_handling/error_handling_test.dart new file mode 100644 index 0000000..048e68f --- /dev/null +++ b/test/augment_test/error_handling/error_handling_test.dart @@ -0,0 +1,78 @@ +import 'package:codable/core.dart'; +import 'package:codable/standard.dart'; +import 'package:test/test.dart'; + +class Data { + const Data(this.value); + final Object? value; +} + +void main() { + group('error handling', () { + test('throws unexpected type error on wrong token', () { + expect( + () => Decodable.fromHandler((decoder) { + return Uri.parse(decoder.decodeString()); + }).fromMap({}), + throwsA(isA().having( + (e) => e.message, + 'message', + 'Failed to decode Uri: Unexpected type: Expected String but got _Map.', + )), + ); + }); + + test('throws unexpected type error on expect call', () { + expect( + () => Decodable.fromHandler((decoder) { + return decoder.expect('String or int'); + }).fromMap({}), + throwsA(isA().having( + (e) => e.message, + 'message', + 'Failed to decode DateTime: Unexpected type: Expected String or int but got _Map.', + )), + ); + }); + + test('throws wrapped exception with decoding path', () { + expect( + () => Decodable.fromHandler((decoder) { + return Data(decoder.decodeMapped().decodeObject( + 'value', + using: Decodable.fromHandler((decoder) { + return Uri.parse(decoder.decodeMapped().decodeString('path')); + }), + )); + }).fromMap({ + 'value': {'path': 42} + }), + throwsA(isA().having( + (e) => e.message, + 'message', + 'Failed to decode Data->["value"]->Uri->["path"]: Unexpected type: Expected String but got int.', + )), + ); + + expect( + () => Decodable.fromHandler((decoder) { + return Data(decoder.decodeMapped().decodeList( + 'values', + using: Decodable.fromHandler((decoder) { + return Uri.parse(decoder.decodeMapped().decodeString('path')); + }), + )); + }).fromMap({ + 'values': [ + {'path': 42} + ] + }), + throwsA(isA().having( + (e) => e.message, + 'message', + 'Failed to decode Data->["values"]->[0]->Uri->["path"]: Unexpected type: Expected String but got int.', + )), + ); + }); + }); +} diff --git a/test/augment_test/generics/basic/basic_test.dart b/test/augment_test/generics/basic/basic_test.dart new file mode 100644 index 0000000..0f32a86 --- /dev/null +++ b/test/augment_test/generics/basic/basic_test.dart @@ -0,0 +1,69 @@ +import 'package:codable/json.dart'; +import 'package:test/test.dart'; + +import '../../basic/model/person.dart'; +import 'model/box.dart'; + +final boxStringJson = '{"label":"name","data":"John"}'; +final boxIntJson = '{"label":"count","data":3}'; +final boxPersonJson = + '{"label":"person","data":{"name":"John","age":30,"height":5.6,"isDeveloper":true,"parent":null,"hobbies":[],"friends":[]}}'; + +void main() { + group('generics', () { + group('basic', () { + test('decodes a box of dynamic', () { + // By default, the codable for a generic class is dynamic. + final Box decoded = Box.codable.fromJson(boxStringJson); + expect(decoded.label, 'name'); + expect(decoded.data, 'John'); + }); + + test('encodes a box of dynamic', () { + final Box box = Box('name', 'John'); + final String encoded = box.toJson(); + expect(encoded, boxStringJson); + }); + + test('decodes a box of string', () { + // The codable for a generic class can be explicitly set to a specific type. + final Box decoded = Box.codable().fromJson(boxStringJson); + expect(decoded.label, 'name'); + expect(decoded.data, 'John'); + }); + + test('encodes a box of string', () { + final Box box = Box('name', 'John'); + final String encoded = box.toJson(); + expect(encoded, boxStringJson); + }); + + test('decodes a box of int', () { + // The codable for a generic class can be explicitly set to a specific type. + final Box decoded = Box.codable().fromJson(boxIntJson); + expect(decoded.label, 'count'); + expect(decoded.data, 3); + }); + + test('encodes a box of int', () { + final Box box = Box('count', 3); + final String encoded = box.toJson(); + expect(encoded, boxIntJson); + }); + + test('decodes a box of person', () { + // For a non-primitive type, the child codable must be explicitly provided. + final Box decoded = Box.codable(Person.codable).fromJson(boxPersonJson); + expect(decoded.label, 'person'); + expect(decoded.data, Person('John', 30, 5.6, true, null, [], [])); + }); + + test('encodes a box of person', () { + final Box box = Box('person', Person('John', 30, 5.6, true, null, [], [])); + // For encoding a non-primitive type, the child codable must be explicitly provided. + final String encoded = box.use(Person.codable).toJson(); + expect(encoded, boxPersonJson); + }); + }); + }); +} diff --git a/test/augment_test/generics/basic/model/box.codable.dart b/test/augment_test/generics/basic/model/box.codable.dart new file mode 100644 index 0000000..ffe8efe --- /dev/null +++ b/test/augment_test/generics/basic/model/box.codable.dart @@ -0,0 +1,45 @@ +part of 'box.dart'; + +//CAUSES_ANALYZER_CRASH?//augment class Box implements SelfEncodable { +//CAUSES_ANALYZER_CRASH?// Box(this.label, this.data); +//CAUSES_ANALYZER_CRASH?// +//CAUSES_ANALYZER_CRASH?// static const Codable1 codable = BoxCodable(); +//CAUSES_ANALYZER_CRASH?// +//CAUSES_ANALYZER_CRASH?// @override +//CAUSES_ANALYZER_CRASH?// void encode(Encoder encoder, [Encodable? encodableT]) { +//CAUSES_ANALYZER_CRASH?// encoder.encodeKeyed() +//CAUSES_ANALYZER_CRASH?// ..encodeString('label', label) +//CAUSES_ANALYZER_CRASH?// ..encodeObject('data', data, using: encodableT) +//CAUSES_ANALYZER_CRASH?// ..end(); +//CAUSES_ANALYZER_CRASH?// } +//CAUSES_ANALYZER_CRASH?//} + +extension BoxEncodableExtension on Box { + SelfEncodable use([Encodable? encodableT]) { + return SelfEncodable.fromHandler((e) => encode(e, encodableT)); + } +} + +class BoxCodable extends Codable1, T> { + const BoxCodable(); + + @override + void encode(Box value, Encoder encoder, [Encodable? encodableA]) { + value.encode(encoder, encodableA); + } + + @override + Box decode(Decoder decoder, [Decodable? decodableT]) { + // For simplicity, we don't check the decoder.whatsNext() here. Don't do this for real implementations. + final mapped = decoder.decodeMapped(); + return Box( + mapped.decodeString('label'), + mapped.decodeObject('data', using: decodableT), + ); + } +} + +extension BoxCodableExtension on Codable1 { + // This is a convenience method for creating a BoxCodable with an explicit child codable. + Codable> call<$A>([Codable<$A>? codableA]) => BoxCodable<$A>().use(codableA); +} diff --git a/test/augment_test/generics/basic/model/box.dart b/test/augment_test/generics/basic/model/box.dart new file mode 100644 index 0000000..fa59bb5 --- /dev/null +++ b/test/augment_test/generics/basic/model/box.dart @@ -0,0 +1,59 @@ +import 'package:codable/core.dart'; +import 'package:codable/extended.dart'; + +part 'box.codable.dart'; + +//@Codable() +class Box implements SelfEncodable { +//~AUGMENT_CAUSES_ERROR//class Box { + Box(this.label, this.data); + + final String label; + final T data; +//~AUGMENT_CAUSES_ERROR//} +//~AUGMENT_CAUSES_ERROR//augment class Box implements SelfEncodable { + + static const Codable1 codable = BoxCodable(); + + @override + void encode(Encoder encoder, [Encodable? encodableT]) { + encoder.encodeKeyed() + ..encodeString('label', label) + ..encodeObject('data', data, using: encodableT) + ..end(); + } +} + + +/* +//!USING `augment` causes analyzer to produce weird errors on +``` +The argument type 'Encodable?' can't be assigned to the parameter type 'Encodable?'. dartargument_type_not_assignable +interface.dart(89, 26): Encodable is defined in C:\src\codable_workspace\packages\codable\lib\src\core\interface.dart +interface.dart(89, 26): Encodable is defined in C:\src\codable_workspace\packages\codable\lib\src\core\interface.dart +[Encodable? encodableT] +Type: Encodable? +``` + +class Box { + Box(this.label, this.data); + + final String label; + final T data; +} + + +augment class Box implements SelfEncodable { + + static const Codable1 codable = BoxCodable(); + + @override + void encode(Encoder encoder, [Encodable? encodableT]) { + encoder.encodeKeyed() + ..encodeString('label', label) + ..encodeObject('data', data, using: encodableT) + ..end(); + } +} + +*/ \ No newline at end of file diff --git a/test/augment_test/generics/basic/model/standalone_error.dart b/test/augment_test/generics/basic/model/standalone_error.dart new file mode 100644 index 0000000..93a272b --- /dev/null +++ b/test/augment_test/generics/basic/model/standalone_error.dart @@ -0,0 +1,186 @@ +/* +//!USING `augment` causes analyzer to produce weird errors: + +The argument type 'Encodable?' can't be assigned to the parameter type 'Encodable?'. dartargument_type_not_assignable +interface.dart(89, 26): Encodable is defined in C:\src\codable_workspace\packages\codable\lib\src\core\interface.dart +interface.dart(89, 26): Encodable is defined in C:\src\codable_workspace\packages\codable\lib\src\core\interface.dart +[Encodable? encodableT] +Type: Encodable? + +*/ +//! Augmentation version produces strange `The argument type 'Encodable?' can't be assigned to the parameter type 'Encodable?'.` +//! errors. + +class Box { + Box(this.label, this.data); + + final String label; + final T data; +} + +augment class Box implements SelfEncodable { + static const Codable1 codable = BoxCodable(); + + @override + void encode(Encoder encoder, [Encodable? encodableT]) { + } +} + +/* +//! THIS VERSION without augmentation produces NO ERROR: +class Box implements SelfEncodable { + Box(this.label, this.data); + + final String label; + final T data; + + static const Codable1 codable = BoxCodable(); + + @override + void encode(Encoder encoder, [Encodable? encodableT]) { + } +} +//! END VERSION without augmentation produces NO ERROR: +*/ + + +extension BoxEncodableExtension on Box { + SelfEncodable use([Encodable? encodableT]) { + return SelfEncodable.fromHandler((e) => encode(e, encodableT)); //! <?' can't be assigned to the parameter type 'Encodable?'. + } +} + +class BoxCodable extends Codable1, T> { + const BoxCodable(); + + @override + void encode(Box value, Encoder encoder, [Encodable? encodableA]) { + value.encode(encoder, encodableA); //! <?' can't be assigned to the parameter type 'Encodable?'. + } + + @override + external Box decode(Decoder decoder, [Decodable? decodableT]); +} + + + + +// SUPPORTING CLASSES + + +abstract interface class Decodable { + /// Decodes a value of type [T] using the [decoder]. + /// + /// The implementation should first use [Decoder.whatsNext] to determine the type of the encoded data. + /// Then it should use one of the [Decoder]s `.decode...()` methods to decode into its target type. + /// If the returned [DecodingType] is not supported, the implementation can use [Decoder.expect] to throw a detailed error. + T decode(Decoder decoder); + + /// Creates a [Decodable] from a handler function. + factory Decodable.fromHandler(T Function(Decoder decoder) decode) => _DecodableFromHandler(decode); +} +abstract interface class Decoder { +} + +/// Variant of [Decodable] that decodes a generic value of type [T] with one type parameter [A]. +abstract interface class Decodable1 implements Decodable { + /// Decodes a value of type [T] using the [decoder]. + /// + /// The implementation should first use [Decoder.whatsNext] to determine the type of the encoded data. + /// Then it should use one of the typed [Decoder].decode...() methods to decode into its target type. + /// If the returned [DecodingType] is not supported, the implementation can use [Decoder.expect] to throw a detailed error. + /// + /// The [decodableA] parameter should be used to decode nested values of type [A]. When it is `null` the + /// implementation may choose to throw an error or use a fallback way of decoding values of type [A]. + @override + T decode(Decoder decoder, [Decodable? decodableA]); +} + + +abstract interface class Encodable1 implements Encodable { + @override + void encode(T value, Encoder encoder, [Encodable? encodableA]); +} + + +abstract class Codable implements Encodable, Decodable { + const Codable(); + + /// Creates a [Codable] from a pair of handler functions. + factory Codable.fromHandlers({ + required T Function(Decoder decoder) decode, + required void Function(T value, Encoder encoder) encode, + }) => + _CodableFromHandlers(decode, encode); +} + +/// Variant of [Codable] that can encode and decode a generic value of type [T] with one type parameter [A]. +abstract class Codable1 implements Codable, Decodable1, Encodable1 { + const Codable1(); +} + +final class _DecodableFromHandler implements Decodable { + const _DecodableFromHandler(this._decode); + + final T Function(Decoder decoder) _decode; + + @override + T decode(Decoder decoder) => _decode(decoder); +} + +final class _EncodableFromHandler implements Encodable { + const _EncodableFromHandler(this._encode); + + final void Function(T value, Encoder encoder) _encode; + + @override + void encode(T value, Encoder encoder) => _encode(value, encoder); +} + +final class _CodableFromHandlers implements Codable { + const _CodableFromHandlers(this._decode, this._encode); + + final T Function(Decoder decoder) _decode; + final void Function(T value, Encoder encoder) _encode; + + @override + T decode(Decoder decoder) => _decode(decoder); + @override + void encode(T value, Encoder encoder) => _encode(value, encoder); +} + +abstract interface class Encodable { + /// Encodes the [value] using the [encoder]. + /// + /// The implementation must use one of the typed [Encoder]s `.encode...()` methods to encode the value. + /// It is expected to call exactly one of the encoding methods a single time. Never more or less. + void encode(T value, Encoder encoder); + + /// Creates an [Encodable] from a handler function. + factory Encodable.fromHandler(void Function(T value, Encoder encoder) encode) => _EncodableFromHandler(encode); +} + +abstract interface class Encoder { +} + +final class _SelfEncodableFromHandler implements SelfEncodable { + const _SelfEncodableFromHandler(this._encode); + + final void Function(Encoder encoder) _encode; + + @override + void encode(Encoder encoder) => _encode(encoder); +} + +abstract interface class SelfEncodable { + /// Encodes itself using the [encoder]. + /// + /// The implementation should use one of the typed [Encoder]s `.encode...()` methods to encode the value. + /// It is expected to call exactly one of the encoding methods a single time. Never more or less. + void encode(Encoder encoder); + + /// Creates a [SelfEncodable] from a handler function. + factory SelfEncodable.fromHandler(void Function(Encoder encoder) encode) => _SelfEncodableFromHandler(encode); +} + + diff --git a/test/augment_test/hooks/delegate.dart b/test/augment_test/hooks/delegate.dart new file mode 100644 index 0000000..7794b94 --- /dev/null +++ b/test/augment_test/hooks/delegate.dart @@ -0,0 +1,95 @@ +import 'package:codable/core.dart'; + +abstract class RecursiveDelegatingDecoder implements Decoder { + RecursiveDelegatingDecoder(this.delegate); + final Decoder delegate; + + RecursiveDelegatingDecoder wrap(Decoder decoder); + + @override + DecodingType whatsNext() => delegate.whatsNext(); + + @override + bool decodeBool() => delegate.decodeBool(); + + @override + bool? decodeBoolOrNull() => delegate.decodeBoolOrNull(); + + @override + int decodeInt() => delegate.decodeInt(); + + @override + int? decodeIntOrNull() => delegate.decodeIntOrNull(); + + @override + double decodeDouble() => delegate.decodeDouble(); + + @override + double? decodeDoubleOrNull() => delegate.decodeDoubleOrNull(); + + @override + num decodeNum() => delegate.decodeNum(); + + @override + num? decodeNumOrNull() => delegate.decodeNumOrNull(); + + @override + String decodeString() => delegate.decodeString(); + + @override + String? decodeStringOrNull() => delegate.decodeStringOrNull(); + + @override + bool decodeIsNull() => delegate.decodeIsNull(); + + @override + T decodeObject({Decodable? using}) => delegate.decodeObject(using: using?.wrap(this)); + + @override + T? decodeObjectOrNull({Decodable? using}) => delegate.decodeObjectOrNull(using: using?.wrap(this)); + + @override + List decodeList({Decodable? using}) => delegate.decodeList(using: using?.wrap(this)); + + @override + List? decodeListOrNull({Decodable? using}) => delegate.decodeListOrNull(using: using?.wrap(this)); + + @override + Map decodeMap({Decodable? keyUsing, Decodable? valueUsing}) => + delegate.decodeMap(keyUsing: keyUsing?.wrap(this), valueUsing: valueUsing?.wrap(this)); + + @override + Map? decodeMapOrNull({Decodable? keyUsing, Decodable? valueUsing}) => + delegate.decodeMapOrNull(keyUsing: keyUsing?.wrap(this), valueUsing: valueUsing?.wrap(this)); + + @override + IteratedDecoder decodeIterated() => delegate.decodeIterated(); + + @override + KeyedDecoder decodeKeyed() => delegate.decodeKeyed(); + + @override + MappedDecoder decodeMapped() => delegate.decodeMapped(); + + @override + bool isHumanReadable() => delegate.isHumanReadable(); + + @override + Never expect(String expect) => delegate.expect(expect); +} + +extension _Wrap on Decodable { + Decodable wrap(RecursiveDelegatingDecoder decoder) => _WrappedDecodable(this, decoder); +} + +class _WrappedDecodable implements Decodable { + _WrappedDecodable(this._decodable, this._parent); + + final Decodable _decodable; + final RecursiveDelegatingDecoder _parent; + + @override + T decode(Decoder decoder) { + return _decodable.decode(_parent.wrap(decoder)); + } +} diff --git a/test/augment_test/hooks/hooks.dart b/test/augment_test/hooks/hooks.dart new file mode 100644 index 0000000..8013681 --- /dev/null +++ b/test/augment_test/hooks/hooks.dart @@ -0,0 +1,85 @@ +import 'package:codable/core.dart'; + +import 'delegate.dart'; + +extension CodableHookExtension on Codable { + /// Returns a [Codable] that applies the provided [Hook] when encoding and decoding [T]. + Codable hook(Hook hook) => CodableHook(this, hook); +} + +/// An object that can be used to modify the encoding and decoding behavior of a type of data format. +abstract mixin class Hook { + /// Called before decoding a value of type [T] using the [decoder] and [decodable]. + /// + /// The implementation may modify the decoding process by wrapping the [decoder] or [decodable], or + /// by providing a custom decoding implementation. + /// + /// To forward to the original implementation, call `super.decode(decoder, decodable)`. + T decode(Decoder decoder, Decodable decodable) => decodable.decode(decoder); + + /// Called before encoding a value of type [T] using the [encoder] and [encodable]. + /// + /// The implementation may modify the encoding process by wrapping the [encoder] or [encodable], or + /// by providing a custom encoding implementation. + /// + /// To forward to the original implementation, call `super.encode(value, encoder, encodable)`. + void encode(T value, Encoder encoder, Encodable encodable) => encodable.encode(value, encoder); +} + +class CodableHook implements Codable { + const CodableHook(this.codable, this.hook); + + final Codable codable; + final Hook hook; + + @override + T decode(Decoder decoder) { + return hook.decode(decoder, codable); + } + + @override + void encode(T value, Encoder encoder) { + hook.encode(value, encoder, codable); + } +} + +abstract mixin class ProxyHook implements Hook { + @override + T decode(Decoder decoder, Decodable decodable) { + return decodable.decode(ProxyDecoder(decoder, this)); + } + + String visitString(String value) => value; + String? visitStringOrNull(String? value) => value; + + @override + void encode(T value, Encoder encoder, Encodable encodable) { + encoder.encodeObject(value, using: encodable); + } +} + +class ProxyDecoder extends RecursiveDelegatingDecoder { + ProxyDecoder(super.wrapped, this.hook); + + final ProxyHook hook; + + @override + String decodeString() { + return hook.visitString(super.decodeString()); + } + + @override + String? decodeStringOrNull() { + return hook.visitStringOrNull(super.decodeStringOrNull()); + } + + @override + Decoder clone() { + return ProxyDecoder(delegate.clone(), hook); + } + + @override + ProxyDecoder wrap(Decoder decoder) { + return ProxyDecoder(decoder, hook); + } +} diff --git a/test/augment_test/mappable/mapper.dart b/test/augment_test/mappable/mapper.dart new file mode 100644 index 0000000..53a2052 --- /dev/null +++ b/test/augment_test/mappable/mapper.dart @@ -0,0 +1,218 @@ +import 'package:codable/src/core/interface.dart'; +import 'package:type_plus/type_plus.dart'; +import 'dart:async'; + +import 'package:codable/common.dart'; +import 'package:codable/core.dart'; +import 'package:codable/extended.dart'; +// ignore: implementation_imports +import 'package:type_plus/src/types_registry.dart' show TypeRegistry; + +abstract class Mapper { + const Mapper(); + + /// A unique id for this type, defaults to the name of the type. + /// + /// Override this if you have two types with the same name. + String get id => T.name; + + /// A type factory is what makes generic types work. + Function get typeFactory => (f) => f(); + + /// A getter for the type of this mapper. + Type get type => T; + + bool isFor(dynamic v) => v is T; + bool isForType(Type type) => type.base == T; +} + +abstract interface class CodableMapper implements Mapper { + Codable get codable; +} + +abstract interface class CodableMapper1 implements Mapper { + Codable codable([Codable? codableA]); +} + +abstract interface class CodableMapper2 implements Mapper { + Codable codable([Codable? codableA, Codable? codableB]); +} + + +Decodable findDecodableFor() { + if (T == List || isBounded()) { + final decodable = T.args.call1(() { + return ListCodable(findCodableFor()!); + }); + if (decodable is Decodable) { + return decodable as Decodable; + } + } + return findCodableFor()!; +} + +Codable? findCodableFor() { + final mapper = MapperContainer.current.findByType(); + return getCodableOf(mapper!); +} + +Codable? getCodableOf(Mapper mapper) { + return switch (mapper) { + CodableMapper m => m.codable, + CodableMapper1 m => T.args.call1(() => m.codable(findCodableFor())), + CodableMapper2 m => T.args.call2(() => m.codable(findCodableFor(), findCodableFor())), + _ => null, + } as Codable?; +} + +Encodable? findEncodeFor(T value) { + if (value is SelfEncodable) return null; + + final mapper = MapperContainer.current.findByValue(value); + return getCodableOf(mapper!)!; +} + +extension on List { + R call1(R Function() fn) { + return first.provideTo(fn); + } + + R call2(R Function() fn) { + return first.provideTo(() => this[1].provideTo(() => fn())); + } +} + +R useMappers(R Function() callback, {List? mappers}) { + return runZoned(callback, zoneValues: { + MapperContainer._containerKey: MapperContainer._inherit(mappers: mappers), + }); +} + +class MapperContainer implements TypeProvider { + static final _containerKey = Object(); + static final _root = MapperContainer._({}); + + static MapperContainer get current => Zone.current[_containerKey] as MapperContainer? ?? _root; + + static MapperContainer _inherit({List? mappers}) { + var parent = current; + if (mappers == null) { + return parent; + } + + return MapperContainer._({ + ...parent._mappers, + for (final m in mappers) m.type: m, + }); + } + + MapperContainer._(this._mappers) { + TypeRegistry.instance.register(this); + } + + final Map _mappers; + + final Map _cachedMappers = {}; + final Map _cachedTypeMappers = {}; + + final Map _cachedObjects = {}; + + Mapper? findByType([Type? type]) { + return _mapperForType(type ?? T); + } + + Mapper? findByValue(T value) { + return _mapperForValue(value); + } + + List> findAll() { + return _mappers.values.whereType>().toList(); + } + + Mapper? _mapperForValue(dynamic value) { + var type = value.runtimeType; + if (_cachedMappers[type] != null) { + return _cachedMappers[type]; + } + var baseType = type.base; + if (baseType == UnresolvedType) { + baseType = type; + } + if (_cachedMappers[baseType] != null) { + return _cachedMappers[baseType]; + } + + var mapper = // + // direct type + _mappers[baseType] ?? + // indirect type ie. subtype + _mappers.values.where((m) => m.isFor(value)).firstOrNull; + + if (mapper != null) { + // if (mapper is ClassMapperBase) { + // mapper = mapper.subOrSelfFor(value) ?? mapper; + // } + if (baseType == mapper.type) { + _cachedMappers[baseType] = mapper; + } else { + _cachedMappers[type] = mapper; + } + } + + return mapper; + } + + Mapper? _mapperForType(Type type) { + if (_cachedTypeMappers[type] case var m?) { + return m; + } + var baseType = type.base; + if (baseType == UnresolvedType) { + baseType = type; + } + if (_cachedTypeMappers[baseType] case var m?) { + return m; + } + var mapper = _mappers[baseType] ?? _mappers.values.where((m) => m.isForType(type)).firstOrNull; + + if (mapper != null) { + if (baseType == mapper.type) { + _cachedTypeMappers[baseType] = mapper; + } else { + _cachedTypeMappers[type] = mapper; + } + } + return mapper; + } + + @override + Function? getFactoryById(String id) { + return _mappers.values.where((m) => m.id == id).firstOrNull?.typeFactory; + } + + @override + List getFactoriesByName(String name) { + return [ + ..._mappers.values.where((m) => m.type.name == name).map((m) => m.typeFactory), + ]; + } + + @override + String? idOf(Type type) { + return _mappers[type]?.id; + } + + T? getCached(Object key) { + return _cachedObjects[key] as T?; + } + + void setCached(Object key, T value) { + _cachedObjects[key] = value; + } +} + + +List> findDiscriminatorFor() { + var mappers = MapperContainer.current.findAll(); + return mappers.map((m) => getCodableOf(m)).whereType>().toList(); +} \ No newline at end of file diff --git a/test/augment_test/mappable/simple.dart b/test/augment_test/mappable/simple.dart new file mode 100644 index 0000000..3088775 --- /dev/null +++ b/test/augment_test/mappable/simple.dart @@ -0,0 +1,49 @@ +import 'package:codable/core.dart'; + +import 'mapper.dart'; + +abstract class SimpleMapper extends Mapper implements CodableMapper, Codable { + const SimpleMapper(); + + @override + Codable get codable => this; + + @override + T decode(Decoder decoder); + @override + void encode(T value, Encoder encoder); +} + +abstract class SimpleMapper1 extends Mapper implements CodableMapper1 { + const SimpleMapper1(); + + @override + Codable codable([Codable? codableA]) => Codable.fromHandlers( + decode: (d) => decode(d, codableA), + encode: (v, e) => encode(v, e, codableA), + ); + + T decode(Decoder decoder, [Decodable? decodableA]); + void encode(covariant T value, Encoder encoder, [Encodable? encodableA]); + + @override + Function get typeFactory; +} + +abstract class SimpleMapper2 extends Mapper implements CodableMapper2 { + const SimpleMapper2(); + + @override + Codable codable([Codable? codableA, Codable? codableB]) { + return Codable.fromHandlers( + decode: (d) => decode(d, codableA, codableB), + encode: (v, e) => encode(v, e, codableA, codableB), + ); + } + + T decode(Decoder decoder, [Decodable? decodableA, Decodable? decodableB]); + void encode(covariant T value, Encoder encoder, [Encodable? encodableA, Encodable? encodableB]); + + @override + Function get typeFactory; +} diff --git a/test/augment_test/polymorphism/basic/model/pet.codable.dart b/test/augment_test/polymorphism/basic/model/pet.codable.dart new file mode 100644 index 0000000..f9cd1e6 --- /dev/null +++ b/test/augment_test/polymorphism/basic/model/pet.codable.dart @@ -0,0 +1,86 @@ +part of 'pet.dart'; + +augment class Pet implements SelfEncodable { + Pet({required this.type}); + + static const Codable codable = PetCodable(); + + @override + void encode(Encoder encoder) { + encoder.encodeKeyed() + ..encodeString('type', type) + ..end(); + } +} + +augment class Cat { + static const Codable codable = CatCodable(); + + @override + void encode(Encoder encoder) { + encoder.encodeKeyed() + ..encodeString('type', type) + ..encodeString('name', name) + ..encodeInt('lives', lives) + ..end(); + } +} + +augment class Dog { + static const Codable codable = DogCodable(); + + @override + Object? encode(Encoder encoder) { + return encoder.encodeKeyed() + ..encodeString('type', type) + ..encodeString('name', name) + ..encodeString('breed', breed) + ..end(); + } +} + +// Pet + +class PetCodable extends SelfCodable with SuperDecodable { + const PetCodable(); + + @override + String get discriminatorKey => 'type'; + + @override + List> get discriminators => [ + Discriminator('cat', CatCodable.new), + Discriminator('dog', DogCodable.new), + ]; + + @override + Pet decodeFallback(Decoder decoder) { + return Pet(type: decoder.decodeMapped().decodeString('type')); + } +} + +class CatCodable extends SelfCodable { + const CatCodable(); + + @override + Cat decode(Decoder decoder) { + final keyed = decoder.decodeMapped(); + return Cat( + name: keyed.decodeString('name'), + lives: keyed.decodeInt('lives'), + ); + } +} + +class DogCodable extends SelfCodable { + const DogCodable(); + + @override + Dog decode(Decoder decoder) { + final keyed = decoder.decodeMapped(); + return Dog( + name: keyed.decodeString('name'), + breed: keyed.decodeString('breed'), + ); + } +} diff --git a/test/augment_test/polymorphism/basic/model/pet.dart b/test/augment_test/polymorphism/basic/model/pet.dart new file mode 100644 index 0000000..b036b8a --- /dev/null +++ b/test/augment_test/polymorphism/basic/model/pet.dart @@ -0,0 +1,26 @@ +import 'package:codable/core.dart'; +import 'package:codable/extended.dart'; + +part 'pet.codable.dart'; + +//@Codable() //!(Constructor not defined, created in augmentation) +class Pet { + final String type; +} + +//@Codable() //!(Here we define constructor) +class Cat extends Pet { + Cat({required this.name, this.lives = 7}) : super(type: 'cat'); + + final String name; + final int lives; +} + +//@Codable() //!(Here we define constructor) +class Dog extends Pet { + Dog({required this.name, required this.breed}) : super(type: 'dog'); + + final String name; + final String breed; +} + diff --git a/test/augment_test/polymorphism/basic/poly_test.dart b/test/augment_test/polymorphism/basic/poly_test.dart new file mode 100644 index 0000000..b1e1a3c --- /dev/null +++ b/test/augment_test/polymorphism/basic/poly_test.dart @@ -0,0 +1,41 @@ +import 'package:codable/standard.dart'; +import 'package:test/test.dart'; + +import 'model/pet.dart'; + +final dogMap = {'name': 'Jasper', 'breed': 'Australian Shepherd', 'type': 'dog'}; +final catMap = {'name': 'Whiskers', 'lives': 5, 'type': 'cat'}; +final birdMap = {'color': 'red', 'type': 'bird'}; + +void main() { + group('polymorphism', () { + test('decodes explicit subtype', () { + Dog dog = Dog.codable.fromMap(dogMap); + expect(dog.name, 'Jasper'); + }); + + test('encodes explicit subtype', () { + Dog dog = Dog(name: 'Jasper', breed: 'Australian Shepherd'); + Map map = dog.toMap(); + expect(map, dogMap); + }); + + test('decodes discriminated subtype', () { + Pet pet = Pet.codable.fromMap(dogMap); + expect(pet, isA()); + expect((pet as Dog).name, "Jasper"); + }); + + test('encodes base type', () { + Pet pet = Dog(name: 'Jasper', breed: 'Australian Shepherd'); + Map map = pet.toMap(); + expect(map, dogMap); + }); + + test('decodes default on unknown key', () { + Pet pet = Pet.codable.fromMap(birdMap); + expect(pet.runtimeType, Pet); + expect(pet.type, 'bird'); + }); + }); +} diff --git a/test/augment_test/polymorphism/complex/complex_poly_test.dart b/test/augment_test/polymorphism/complex/complex_poly_test.dart new file mode 100644 index 0000000..0602445 --- /dev/null +++ b/test/augment_test/polymorphism/complex/complex_poly_test.dart @@ -0,0 +1,284 @@ +import 'package:codable/common.dart'; +import 'package:codable/core.dart'; +import 'package:codable/extended.dart'; +import 'package:codable/standard.dart'; +import 'package:test/test.dart'; + +import 'model/box.dart'; + +final labelBoxMap = {'type': 'label', 'content': "some label"}; +final anyBoxMap = { + 'type': 'any', + 'content': {'value': 'some value'} +}; +final numberBoxMap = {'type': 'number', 'content': 2.5}; +final boxesMap = { + 'type': 'boxes', + 'content': [ + {'value': 1}, + {'value': 2}, + {'value': 3} + ] +}; +final metaBoxMap = {'type': 'meta', 'metadata': 2, 'content': 'test'}; +final metaDataBoxMap = { + 'type': 'meta', + 'metadata': 2, + 'content': {'value': 'test'} +}; +final higherOrderLabelBoxMap = {'type': 'higher_order', 'metadata': 'some metadata', 'content': labelBoxMap}; +final higherOrderNumberBoxMap = {'type': 'higher_order', 'metadata': 100, 'content': numberBoxMap}; + +void main() { + group('complex polymorphism', () { + group("with reduced type parameter", () { + test('decodes LabelBox as Box', () { + final Codable> boxCodable = BoxCodable(); + final Box box = boxCodable.fromMap(labelBoxMap); + expect(box, isA()); + expect(box.content, 'some label'); + }); + + test('decodes LabelBox as Box', () { + final Codable> boxCodable = BoxCodable(); + final Box box = boxCodable.fromMap(labelBoxMap); + expect(box, isA()); + expect(box.content, 'some label'); + }); + + test('decodes LabelBox not as Box', () { + final Codable> boxCodable = BoxCodable(); + expect( + () { + // ignore: unused_local_variable + final Box box = boxCodable.fromMap(labelBoxMap); + }, + throwsA(isA().having( + (e) => e.message, + 'message', + 'Failed to decode Box: Cannot resolve discriminator to decode type Box. Got LabelBoxCodable.', + )), + ); + }); + + test('encodes LabelBox', () { + final LabelBox box = LabelBox('some label'); + final Map map = box.toMap(); + expect(map, labelBoxMap); + }); + + test('decodes AnyBox as Box', () { + final Codable> boxCodable = BoxCodable(); + final Box box = boxCodable.fromMap(anyBoxMap); + expect(box, isA()); + expect(box.content, {'value': 'some value'}); + }); + + test('encodes AnyBox', () { + final AnyBox box = AnyBox({'value': 'some value'}); + final Map map = box.toMap(); + expect(map, anyBoxMap); + }); + }); + + group("with bounded type parameter", () { + test('decodes NumberBox as Box', () { + final Codable> boxCodable = BoxCodable(); + final Box box = boxCodable.fromMap(numberBoxMap); + expect(box, isA()); + expect(box.content, 2.5); + }); + test('decodes NumberBox as Box', () { + final Codable> boxCodable = BoxCodable(); + final Box box = boxCodable.fromMap(numberBoxMap); + expect(box, isA()); + expect(box.content, 2.5); + }); + test('decodes NumberBox as Box', () { + final Codable> boxCodable = BoxCodable(); + final Box box = boxCodable.fromMap(numberBoxMap); + expect(box, isA>()); + expect(box.content, 2.5); + }); + + test('decodes NumberBox not as Box', () { + final Codable> boxCodable = BoxCodable(); + expect( + () { + // ignore: unused_local_variable + final Box box = boxCodable.fromMap(numberBoxMap); + }, + throwsA(isA().having( + (e) => e.message, + 'message', + 'Failed to decode Box: Cannot resolve discriminator to decode type Box. Got _UseDecodable1, num>.', + )), + ); + }); + + test('encodes NumberBox', () { + final NumberBox box = NumberBox(2.5); + final Map map = box.toMap(); + expect(map, numberBoxMap); + }); + }); + + group("with composed type parameter", () { + test('decodes Boxes as Box', () { + final Codable> boxCodable = BoxCodable(); + final Box box = boxCodable.fromMap(boxesMap); + expect(box, isA>()); + expect(box.content, [ + {'value': 1}, + {'value': 2}, + {'value': 3} + ]); + }); + + test('decodes Boxes as Box>', () { + final Codable> boxCodable = BoxCodable(); + final Box box = boxCodable.fromMap(boxesMap); + expect(box, isA>()); + expect(box.content, [ + {'value': 1}, + {'value': 2}, + {'value': 3} + ]); + }); + + test('decodes Boxes as Box> with explicit child decode', () { + final Codable>> boxCodable = BoxCodable>().use(Data.codable.list()); + final Box> box = boxCodable.fromMap(boxesMap); + expect(box, isA>()); + expect(box.content, [ + Data(1), + Data(2), + Data(3), + ]); + }); + + test('decodes Boxes not as Box> without explicit child decode', () { + final Codable>> boxCodable = BoxCodable>().use(); + + expect( + () { + // ignore: unused_local_variable + final Box> box = boxCodable.fromMap(boxesMap); + }, + throwsA(isA().having( + (e) => e.message, + 'message', + 'Failed to decode Box>: Cannot resolve discriminator to decode type Box>. Got BoxesCodable.', + )), + ); + }); + + test('encodes Boxes without explicit inner encodable', () { + final Boxes box = Boxes([ + {'value': 1}, + {'value': 2}, + {'value': 3} + ]); + final Map map = box.toMap(); + expect(map, boxesMap); + }); + + test('encodes Boxes with explicit inner encodable', () { + final Boxes box = Boxes([ + Data(1), + Data(2), + Data(3), + ]); + final Map map = box.use(Data.codable).toMap(); + expect(map, boxesMap); + }); + + test('encodes Boxes as Box> with explicit inner encodable', () { + final Box> box = Boxes([ + Data(1), + Data(2), + Data(3), + ]); + final Map map = box.use(Data.codable.list()).toMap(); + expect(map, boxesMap); + }); + }); + + group("with additional type parameter", () { + test('decodes MetaBox as Box', () { + final Codable> boxCodable = BoxCodable(); + final Box box = boxCodable.fromMap(metaBoxMap); + expect(box, isA>()); + expect((box as MetaBox).metadata, 2); + expect(box.content, 'test'); + }); + + test('decodes MetaBox as Box ', () { + final Codable> boxCodable = BoxCodable(); + final Box box = boxCodable.fromMap(metaBoxMap); + expect(box, isA>()); + expect((box as MetaBox).metadata, 2); + expect(box.content, 'test'); + }); + + test('decodes MetaBox as Box with explicit child decode', () { + final Codable> boxCodable = BoxCodable().use(Data.codable); + final Box box = boxCodable.fromMap(metaDataBoxMap); + expect(box, isA>()); + expect((box as MetaBox).metadata, 2); + expect(box.content, Data('test')); + }); + + test('encodes MetaBox with explicit inner encodable', () { + final MetaBox box = MetaBox(2, Data('test')); + final Map map = box.use(null, Data.codable).toMap(); + expect(map, metaDataBoxMap); + }); + + test('decodes HigherOrderBox as Box', () { + final Codable> boxCodable = BoxCodable(); + final Box box = boxCodable.fromMap(higherOrderLabelBoxMap); + expect(box, isA>()); + expect((box as HigherOrderBox).metadata, 'some metadata'); + expect(box.content, isA()); + expect(box.content.content, 'some label'); + }); + + test('decodes HigherOrderBox as Box>', () { + final Codable>> boxCodable = BoxCodable>(); + final Box> box = boxCodable.fromMap(higherOrderLabelBoxMap); + expect(box, isA>()); + expect((box as HigherOrderBox).metadata, 'some metadata'); + expect(box.content, isA()); + expect(box.content.content, 'some label'); + }); + + test('decodes HigherOrderBox as Box>', () { + final Codable>> boxCodable = BoxCodable>(); + final Box> box = boxCodable.fromMap(higherOrderLabelBoxMap); + expect(box, isA>>()); + expect((box as HigherOrderBox).metadata, 'some metadata'); + expect(box.content, isA()); + expect(box.content.content, 'some label'); + }); + + test('decodes HigherOrderBox as Box', () { + final Codable> boxCodable = BoxCodable(); + final Box box = boxCodable.fromMap(higherOrderLabelBoxMap); + expect(box, isA>()); + expect((box as HigherOrderBox).metadata, 'some metadata'); + expect(box.content, isA()); + expect(box.content.content, 'some label'); + }); + + test('decodes HigherOrderBox as Box', () { + final Codable> boxCodable = BoxCodable(); + final Box box = boxCodable.fromMap(higherOrderNumberBoxMap); + expect(box, isA>()); + expect((box as HigherOrderBox).metadata, 100); + expect(box.content, isA()); + expect(box.content.content, 2.5); + }); + }); + }); +} diff --git a/test/augment_test/polymorphism/complex/model/box.codable.dart b/test/augment_test/polymorphism/complex/model/box.codable.dart new file mode 100644 index 0000000..6c81d22 --- /dev/null +++ b/test/augment_test/polymorphism/complex/model/box.codable.dart @@ -0,0 +1,325 @@ +part of 'box.dart'; + +/* CAUSING ANALYZER CRASHES + +augment class Box implements SelfEncodable { + const Box(this.content); + + @override + void encode(Encoder encoder, [Encodable? encodableT]) { + encoder.encodeKeyed() + ..encodeObject('content', content, using: encodableT) + ..end(); + } +} + + + +// For testing fixed type parameters. +augment class LabelBox { + @override + void encode(Encoder encoder, [_]) { + encoder.encodeKeyed() + ..encodeString('type', 'label') + ..encodeString('content', content) + ..end(); + } +} + +// For testing reduced type parameters. +augment class AnyBox { + @override + void encode(Encoder encoder, [_]) { + encoder.encodeKeyed() + ..encodeString('type', 'any') + ..encodeObject('content', content) + ..end(); + } +} + +// For testing bounded type parameters. +augment class NumberBox { + @override + void encode(Encoder encoder, [_]) { + encoder.encodeKeyed() + ..encodeString('type', 'number') + ..encodeNum('content', content) + ..end(); + } +} + +// For testing nested type parameters. +augment class Boxes { + @override + void encode(Encoder encoder, [Encodable>? encodableT, Encodable? encodableT2]) { + final keyed = encoder.encodeKeyed(); + keyed.encodeString('type', 'boxes'); + if (encodableT2 == null && encodableT != null) { + keyed.encodeObject('content', content, using: encodableT); + } else { + keyed.encodeIterable('content', content, using: encodableT2); + } + keyed.end(); + } +} + + +CAUSING ANALYZER CRASHES */ + +extension BoxEncodableExtension on Box { + SelfEncodable use([Encodable? encodableT]) { + return SelfEncodable.fromHandler((e) => encode(e, encodableT)); + } +} + +extension BoxesEncodableExtension on Boxes { + SelfEncodable use([Encodable? encodableT]) { + return SelfEncodable.fromHandler((e) => encode(e, null, encodableT)); + } +} + + +/* USING THIS AUGMENT does not cause crash but does cause the weird error below on MetaBoxEncodableExtension + +// For testing additional type parameters. +augment class MetaBox { + @override + void encode(Encoder encoder, [Encodable? encodableT, Encodable? encodableV]) { + encoder.encodeKeyed() + ..encodeString('type', 'meta') + ..encodeObject('metadata', metadata, using: encodableV) + ..encodeObject('content', content, using: encodableT) + ..end(); + } +} + +CAUSES ERROR BELOW */ + +/* FOLLOWING GETS ERROR: + +The argument type 'Encodable?' can't be assigned to the parameter type 'Encodable?'. dartargument_type_not_assignable +interface.dart(89, 26): Encodable is defined in C:\src\codable_workspace\packages\codable\lib\src\core\interface.dart +interface.dart(89, 26): Encodable is defined in C:\src\codable_workspace\packages\codable\lib\src\core\interface.dart +[Encodable? encodableT] +Type: Encodable? + +when using augment version + +*/ + +extension MetaBoxEncodableExtension on MetaBox { + SelfEncodable use([Encodable? encodableV, Encodable? encodableT]) { + return SelfEncodable.fromHandler((e) => encode(e, encodableT, encodableV)); + } +} + +/* WE GET ANALYZER ERROR:class +The augmentation type parameter must have the same bound as the corresponding type parameter of the declaration. +Try changing the augmentation to match the declaration type parameters.dart(augmentation_type_parameter_bound) +T +test/augment_test/polymorphism/complex/model/box.dart + + + + +// For testing self-dependent type parameters. +//augment class HigherOrderBox> { +augment class HigherOrderBox> { + @override + void encode(Encoder encoder, [Encodable? encodableB, Encodable? encodableT]) { + encoder.encodeKeyed() + ..encodeString('type', 'higher_order') + ..encodeObject('metadata', metadata, using: encodableT) + ..encodeObject('content', content, using: encodableB) + ..end(); + } +} + +END VERSION THAT CREATES ERROR WHEN using augment */ + + +// Codable implementations +// ======================= +class BoxCodable with SuperDecodable1, T> implements Codable1, T> { + const BoxCodable(); + + @override + String get discriminatorKey => 'type'; + + @override + List> get discriminators => [ + Discriminator.arg1('label', <_>(_) { + return LabelBoxCodable(); + }), + Discriminator.arg1('any', <_>(_) { + return AnyBoxCodable(); + }), + Discriminator.arg1Bounded('number', (Decodable? decodableT) { + return NumberBoxCodable().useDecodable(decodableT); + }), + Discriminator.arg1Bounded('boxes', (Decodable? decodableLT) { + if (decodableLT case ComposedDecodable1 d) { + return d.extract>((decodableT) => BoxesCodable().useDecodable(decodableT)); + } else { + return BoxesCodable(); + } + }), + Discriminator.arg1('meta', (decodableT) { + return MetaBoxCodable().useDecodable(null, decodableT); + }), + ...Discriminator.chain1(MetaBoxCodable().discriminators, (d, decodableT) { + return d.resolve2, dynamic, T>(null, decodableT); + }), + ]; + + @override + void encode(Box value, Encoder encoder, [Encodable? encodableT]) { + value.encode(encoder, encodableT); + } +} + +class LabelBoxCodable implements Codable { + @override + LabelBox decode(Decoder decoder) { + final mapped = decoder.decodeMapped(); + return LabelBox( + mapped.decodeString('content'), + ); + } + + @override + void encode(LabelBox value, Encoder encoder) { + value.encode(encoder); + } +} + +class AnyBoxCodable implements Codable { + @override + AnyBox decode(Decoder decoder, [Decodable? decodableT]) { + final mapped = decoder.decodeMapped(); + return AnyBox( + mapped.decodeObject('content'), + ); + } + + @override + void encode(AnyBox value, Encoder encoder) { + value.encode(encoder); + } +} + +class NumberBoxCodable implements Codable1, T> { + @override + NumberBox decode(Decoder decoder, [Decodable? decodableT]) { + final mapped = decoder.decodeMapped(); + return NumberBox( + mapped.decodeObject('content', using: decodableT), + ); + } + + @override + void encode(NumberBox value, Encoder encoder, [Encodable? encodableT]) { + value.encode(encoder, encodableT); + } +} + +class BoxesCodable implements Codable1, T> { + @override + Boxes decode(Decoder decoder, [Decodable? decodableT]) { + final mapped = decoder.decodeMapped(); + return Boxes( + mapped.decodeList('content', using: decodableT), + ); + } + + @override + void encode(Boxes value, Encoder encoder, [Encodable? encodableT]) { + value.encode(encoder, null, encodableT); + } +} + +class MetaBoxCodable with SuperDecodable2, V, T> implements Codable2, V, T> { + @override + String? get discriminatorKey => 'type'; + + @override + List> get discriminators => [ + Discriminator.arg2Bounded( + 'higher_order', + >(Decodable? decodableT, Decodable? decodableB) { + return HigherOrderBoxCodable().useDecodable(decodableT, decodableB); + }, + ), + ]; + + @override + MetaBox decodeFallback(Decoder decoder, [Decodable? decodableV, Decodable? decodableT]) { + final mapped = decoder.decodeMapped(); + return MetaBox( + mapped.decodeObject('metadata', using: decodableV), + mapped.decodeObject('content', using: decodableT), + ); + } + + @override + void encode(MetaBox value, Encoder encoder, [Encodable? encodableV, Encodable? encodableT]) { + value.encode(encoder, encodableT, encodableV); + } +} + +class HigherOrderBoxCodable> implements Codable2, T, B> { + @override + HigherOrderBox decode(Decoder decoder, [Decodable? decodableT, Decodable? decodableB]) { + final mapped = decoder.decodeMapped(); + return HigherOrderBox( + mapped.decodeObject('metadata', using: decodableT), + decodableB != null // + ? mapped.decodeObject('content', using: decodableB) + : mapped.decodeObject>('content', using: BoxCodable()) as B, + ); + } + + @override + void encode(HigherOrderBox value, Encoder encoder, [Encodable? encodableT, Encodable? encodableB]) { + value.encode(encoder, encodableB, encodableT); + } +} + +// For testing nested types. +augment class Data implements SelfEncodable { + const Data(this.value); + + static const Codable codable = DataCodable(); + + @override + void encode(Encoder encoder) { + encoder.encodeKeyed() + ..encodeObject('value', value) + ..end(); + } +} + +// Additional augmentation for equatable +augment class Data { + @override + bool operator ==(Object other) => + identical(this, other) || other is Data && runtimeType == other.runtimeType && value == other.value; + + @override + int get hashCode => value.hashCode; +} + +// Additional augmentation for toString +augment class Data { + @override + String toString() => 'Data{value: $value}'; +} + +class DataCodable extends SelfCodable { + const DataCodable(); + + @override + Data decode(Decoder decoder) { + return Data(decoder.decodeMapped().decodeObject('value')); + } +} diff --git a/test/augment_test/polymorphism/complex/model/box.dart b/test/augment_test/polymorphism/complex/model/box.dart new file mode 100644 index 0000000..223b63c --- /dev/null +++ b/test/augment_test/polymorphism/complex/model/box.dart @@ -0,0 +1,191 @@ +import 'package:codable/core.dart'; +import 'package:codable/extended.dart'; + +part 'box.codable.dart'; + +/* +//! CAUSING ANALYZER CRASHES + +//@Codable() +abstract class Box { + final T content; +} + + +// For testing fixed type parameters. +//@Codable() //?(Would this annotation be required or desired? could we infer from parent type ?) +class LabelBox extends Box { + const LabelBox(super.content); +} + +// For testing reduced type parameters. +//@Codable() //?(Would this annotation be required or desired? could we infer from parent type ?) +class AnyBox extends Box { + const AnyBox(super.content); +} + +// For testing bounded type parameters. +//@Codable() //?(Would this annotation be required or desired? could we infer from parent type ?) +class NumberBox extends Box { + const NumberBox(super.content); +} + +// For testing nested type parameters. +//@Codable() //?(Would this annotation be required or desired? could we infer from parent type ?) +class Boxes extends Box> { + const Boxes(super.content); +} + +//!CAUSING ANALYZER CRASHES +*/ + + + +//~ START ORIGINAL CODE (WITHOUT AUGMENTATION) FOR ABOVE CAUSING CRASHES + +abstract class Box implements SelfEncodable { + const Box(this.content); + + final T content; + + @override + void encode(Encoder encoder, [Encodable? encodableT]) { + encoder.encodeKeyed() + ..encodeObject('content', content, using: encodableT) + ..end(); + } +} + +// For testing fixed type parameters. +class LabelBox extends Box { + const LabelBox(super.content); + + @override + void encode(Encoder encoder, [_]) { + encoder.encodeKeyed() + ..encodeString('type', 'label') + ..encodeString('content', content) + ..end(); + } +} + +// For testing reduced type parameters. +class AnyBox extends Box { + const AnyBox(super.content); + + @override + void encode(Encoder encoder, [_]) { + encoder.encodeKeyed() + ..encodeString('type', 'any') + ..encodeObject('content', content) + ..end(); + } +} + +// For testing bounded type parameters. +class NumberBox extends Box { + const NumberBox(super.content); + + @override + void encode(Encoder encoder, [_]) { + encoder.encodeKeyed() + ..encodeString('type', 'number') + ..encodeNum('content', content) + ..end(); + } +} + +// For testing nested type parameters. +class Boxes extends Box> { + const Boxes(super.content); + + @override + void encode(Encoder encoder, [Encodable>? encodableT, Encodable? encodableT2]) { + final keyed = encoder.encodeKeyed(); + keyed.encodeString('type', 'boxes'); + if (encodableT2 == null && encodableT != null) { + keyed.encodeObject('content', content, using: encodableT); + } else { + keyed.encodeIterable('content', content, using: encodableT2); + } + keyed.end(); + } +} + +//~ END WORK AROUND FOR CRASH + + + + +/* +//! THESE VERSIONS WITH AUGMENTATION DONT CRASH +but do cause the weird errors in the augmentation + + +// For testing additional type parameters. +//@Codable() //?(Would this annotation be required or desired? could we infer from parent type ?) +class MetaBox extends Box { + const MetaBox(this.metadata, super.content); + + final V metadata; +} + +CAUSES ERROR - using non augmentation version below */ + + +/* ERROR occurs on the augment version:: +The augmentation type parameter must have the same bound as the corresponding type parameter of the declaration. +Try changing the augmentation to match the declaration type parameters.dart(augmentation_type_parameter_bound) +T +test/augment_test/polymorphism/complex/model/box.dart + +// For testing self-dependent type parameters. +//@Codable() //?(Would this annotation be required or desired? could we infer from parent type ?) +class HigherOrderBox> extends MetaBox { + const HigherOrderBox(super.metadata, super.content); +} + +//!!END VERSION that creates ERROR +*/ + + +//~ BEGIN original NON augmentation version +class MetaBox extends Box { + const MetaBox(this.metadata, super.content); + + final V metadata; + + @override + void encode(Encoder encoder, [Encodable? encodableT, Encodable? encodableV]) { + encoder.encodeKeyed() + ..encodeString('type', 'meta') + ..encodeObject('metadata', metadata, using: encodableV) + ..encodeObject('content', content, using: encodableT) + ..end(); + } +} + +// For testing self-dependent type parameters. +class HigherOrderBox> extends MetaBox { + const HigherOrderBox(super.metadata, super.content); + + @override + void encode(Encoder encoder, [Encodable? encodableB, Encodable? encodableT]) { + encoder.encodeKeyed() + ..encodeString('type', 'higher_order') + ..encodeObject('metadata', metadata, using: encodableT) + ..encodeObject('content', content, using: encodableB) + ..end(); + } +} + +//~ END original NON augmentation version to avoid weird analyzer errors + + + +// For testing nested types. +//@Codable(equatable:true,toString:true) //! arguments could be used to trigger creation of equatable and toString() methods +class Data { + final dynamic value; +} + diff --git a/test/augment_test/polymorphism/multi_interface/model/material.codable.dart b/test/augment_test/polymorphism/multi_interface/model/material.codable.dart new file mode 100644 index 0000000..d1929ef --- /dev/null +++ b/test/augment_test/polymorphism/multi_interface/model/material.codable.dart @@ -0,0 +1,71 @@ +part of 'material.dart'; + + +augment abstract class Material { + static const Decodable decodable = MaterialDecodable(); +} + +augment abstract class PeriodicElement { + static const Decodable decodable = PeriodicElementDecodable(); +} + +// Decodable implementations +// ---------------------- +// For this test we only care about decoding, so we only create +// Decodable implementations instead of Codable implementations. + +class MaterialDecodable with SuperDecodable { + const MaterialDecodable(); + + @override + String get discriminatorKey => 'type'; + + @override + List> get discriminators => [ + Discriminator('wood', WoodDecodable.new), + Discriminator('iron', IronDecodable.new), + Discriminator('gold', GoldDecodable.new), + ]; +} + +class PeriodicElementDecodable with SuperDecodable { + const PeriodicElementDecodable(); + + @override + String get discriminatorKey => 'symbol'; + + @override + List> get discriminators => [ + Discriminator('Fe', IronDecodable.new), + Discriminator('Au', GoldDecodable.new), + Discriminator('He', HeliumDecodable.new), + ]; +} + +class WoodDecodable implements Decodable { + @override + Wood decode(Decoder decoder) { + return Wood(); + } +} + +class IronDecodable implements Decodable { + @override + Iron decode(Decoder decoder) { + return Iron(); + } +} + +class GoldDecodable implements Decodable { + @override + Gold decode(Decoder decoder) { + return Gold(); + } +} + +class HeliumDecodable implements Decodable { + @override + Helium decode(Decoder decoder) { + return Helium(); + } +} diff --git a/test/augment_test/polymorphism/multi_interface/model/material.dart b/test/augment_test/polymorphism/multi_interface/model/material.dart new file mode 100644 index 0000000..6d05a15 --- /dev/null +++ b/test/augment_test/polymorphism/multi_interface/model/material.dart @@ -0,0 +1,39 @@ +import 'package:codable/core.dart'; +import 'package:codable/extended.dart'; + +part 'material.codable.dart'; + +// Decodable implementations +// ---------------------- +// For this test we only care about decoding, so we only create +// Decodable implementations instead of Codable implementations. + +//@Decodable() +abstract class Material { + Material(); +} + +//@Decodable() +abstract class PeriodicElement { + PeriodicElement(); +} + +//@Decodable() +class Wood extends Material { + Wood(); +} + +//@Decodable() +class Iron extends Material implements PeriodicElement { + Iron(); +} + +//@Decodable() +class Gold extends PeriodicElement implements Material { + Gold(); +} + +//@Decodable() +class Helium extends PeriodicElement { + Helium(); +} diff --git a/test/augment_test/polymorphism/multi_interface/multi_interface_test.dart b/test/augment_test/polymorphism/multi_interface/multi_interface_test.dart new file mode 100644 index 0000000..c038fee --- /dev/null +++ b/test/augment_test/polymorphism/multi_interface/multi_interface_test.dart @@ -0,0 +1,33 @@ +import 'package:codable/standard.dart'; +import 'package:test/test.dart'; + +import 'model/material.dart'; + +final woodMap = {'type': 'wood'}; +final ironMap = {'type': 'iron', 'symbol': 'Fe'}; +final goldMap = {'type': 'gold', 'symbol': 'Au'}; +final heliumMap = {'symbol': 'He'}; + +void main() { + group('multi polymorphism', () { + test('decodes single discriminated subtype', () { + Material material = Material.decodable.fromMap(woodMap); + expect(material, isA()); + + PeriodicElement element = PeriodicElement.decodable.fromMap(heliumMap); + expect(element, isA()); + }); + + test('decodes multi discriminated subtype', () { + Material material = Material.decodable.fromMap(ironMap); + PeriodicElement element = PeriodicElement.decodable.fromMap(ironMap); + expect(material, isA()); + expect(element, isA()); + + Material material2 = Material.decodable.fromMap(goldMap); + PeriodicElement element2 = PeriodicElement.decodable.fromMap(goldMap); + expect(material2, isA()); + expect(element2, isA()); + }); + }); +}