Skip to content

Commit f87ff68

Browse files
committed
feat: add DogNativeMapReader and ConverterHelperExtensions for improved deserialization
1 parent 376650d commit f87ff68

File tree

7 files changed

+204
-111
lines changed

7 files changed

+204
-111
lines changed

docs/basics/converters.md

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class LatLng {
1717
String toString() => "LatLng($lat, $lng)";
1818
}
1919
20-
@linkSerializer/*(1)!*/
20+
@dogsLinked/*(1)!*/
2121
class LatLngConverter extends SimpleDogConverter<LatLng>/*(2)!*/ {
2222
LatLngConverter() : super(serialName: "LatLng");
2323
@@ -34,13 +34,13 @@ class LatLngConverter extends SimpleDogConverter<LatLng>/*(2)!*/ {
3434
}
3535
```
3636

37-
1. The `@linkSerializer` annotation is used to automatically register the converter in the `DogEngine`.
37+
1. The `@linkSerializer` annotation is used to automatically register compatible extensions in the `DogEngine`.
3838
2. The `SimpleDogConverter` class is a convenience class that implements `DogConverter` and provides
3939
both the NativeSerializerMode and the GraphSerializerMode. It also creates a synthetic structure for
4040
the converter type that uses the `serialName`.
4141

4242
In this example, we created a converter for the `LatLng` class. The converter is registered in the
43-
`DogEngine` using the `@linkSerializer` annotation. The 'SimpleDogConverter' base class is the easiest
43+
`DogEngine` using the `@dogsLinked` annotation. The 'SimpleDogConverter' base class is the easiest
4444
way to create a converter – it implements the `DogConverter` interface and automatically creates a native
4545
serialization mode and a synthetic structure.
4646

@@ -74,6 +74,7 @@ first base type. If the type tree has type arguments, the base converter will mo
7474
recursively**.
7575

7676
``` { .dart title="List Converter using createIterableFactory" }
77+
@dogsLinked
7778
final myListFactory = TreeBaseConverterFactory.createIterableFactory<MyList>(
7879
wrap: <T>(Iterable<T> entries) => MyList(entries.toList()),
7980
unwrap: <T>(MyList value) => value,
@@ -83,17 +84,19 @@ Iterable converters are the most basic and also the most common type of tree con
8384
easy to create and can be used to convert any type of iterable. The `wrap` and `unwrap` functions
8485
are used to convert the iterable to and from the tree's base type.
8586

86-
``` { .dart title="Registering a custom tree base factory" }
87-
dogs.registerTreeBaseFactory(
88-
TypeToken<MyConverterBaseType>(),
89-
myCustomConverterFactory
90-
);
91-
```
87+
??? note "Manual Registration"
88+
You can register a custom tree base factory using the `registerTreeBaseFactory` method of the `DogEngine`.
9289

93-
You can register a custom tree base factory using the `registerTreeBaseFactory` method of the `DogEngine`.
90+
```{ .dart title="Registering a custom tree base factory" }
91+
dogs.registerTreeBaseFactory(
92+
TypeToken<MyConverterBaseType>(),
93+
myCustomConverterFactory
94+
);
95+
```
9496

9597

9698
```{ .dart title="Map Converter using NTreeArgConverter" }
99+
@dogsLinked
97100
final mapFactory = TreeBaseConverterFactory.createNargsFactory<Map>(
98101
nargs: 2, consume: <K, V>() => MapNTreeArgConverter<K, V>()
99102
);
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
library;
2+
3+
import "package:dogs_core/dogs_core.dart";
4+
5+
/// Utility class for converters to access dogs deserialization capabilities when decoding custom
6+
/// map structures.
7+
class DogNativeMapReader {
8+
final DogEngine _engine;
9+
final Map<String, dynamic> _backing;
10+
final DogConverter _converter;
11+
12+
/// Utility class for converters to access dogs deserialization capabilities when decoding custom
13+
/// map structures.
14+
const DogNativeMapReader(this._engine, this._backing, this._converter);
15+
16+
/// Reads a value of type [T] from the backing map by [key].
17+
/// The type parameter [T] must be specified explicitly and cannot be nullable.
18+
///
19+
/// Strategy:
20+
/// 1. If the key is not found and [defaultValue] is provided, returns [defaultValue]. Otherwise,
21+
/// throws [DogSerializerException].
22+
/// 2. If the value is already of type [T], returns it.
23+
/// 3. If [T] is a native type (as per the engine's codec), attempts to coerce the value to [T].
24+
/// 4. Otherwise, uses the engine to deserialize the value into type [T].
25+
T read<T>(String key, [T? defaultValue]) {
26+
final value = _backing[key];
27+
if (value == null) {
28+
if (defaultValue != null) return defaultValue;
29+
throw DogSerializerException(message: "Missing required field '$key'", converter: _converter);
30+
}
31+
if (value is T) return value;
32+
if (_engine.codec.isNative(T)) {
33+
try {
34+
return _engine.codec.primitiveCoercion.coerce(TypeToken<T>(), value, key);
35+
} catch (e) {
36+
throw DogSerializerException(
37+
message: "Failed to coerce field '$key' to type $T: $e",
38+
converter: _converter,
39+
cause: e,
40+
);
41+
}
42+
}
43+
try {
44+
return _engine.fromNative<T>(value);
45+
} catch (e) {
46+
throw DogSerializerException(
47+
message: "Failed to deserialize field '$key' to type $T: $e",
48+
converter: _converter,
49+
cause: e,
50+
);
51+
}
52+
}
53+
54+
/// Reads a value of type [T] from the backing map by [key], expecting it to be exactly of type [T].
55+
T expects<T>(String key, [String? message]) {
56+
final value = _backing[key];
57+
if (value is T) return value;
58+
throw DogSerializerException(
59+
message: message ?? "Expected type $T for field '$key' but got ${value.runtimeType}");
60+
}
61+
62+
/// Direct access to the underlying map to retrieve raw values.
63+
dynamic operator [](String key) => _backing[key];
64+
}
65+
66+
/// Extension methods for [DogConverter] to simplify common deserialization tasks.
67+
extension ConverterHelperExtensions on DogConverter {
68+
/// Converts this object to a [DogNativeMapReader] if it is a [Map<String, dynamic>],
69+
/// otherwise throws a [DogSerializerException].
70+
DogNativeMapReader readAsMap(dynamic input, DogEngine engine) {
71+
if (input is Map<String, dynamic>) {
72+
return DogNativeMapReader(engine, input, this);
73+
}
74+
throw DogSerializerException(message: "Expected a map but got $runtimeType", converter: this);
75+
}
76+
77+
/// Reads a value of type [T] from [value].
78+
T readAs<T>(dynamic value, DogEngine engine, [T? defaultValue]) {
79+
if (value == null) {
80+
if (defaultValue != null) return defaultValue;
81+
throw DogSerializerException(message: "Value is null", converter: this);
82+
}
83+
if (value is T) return value;
84+
if (engine.codec.isNative(T)) {
85+
try {
86+
return engine.codec.primitiveCoercion.coerce(TypeToken<T>(), value, null);
87+
} catch (e) {
88+
throw DogSerializerException(
89+
message: "Failed to coerce value to type $T: $e",
90+
converter: this,
91+
cause: e,
92+
);
93+
}
94+
}
95+
try {
96+
return engine.fromNative<T>(value);
97+
} catch (e) {
98+
throw DogSerializerException(
99+
message: "Failed to deserialize value to type $T: $e",
100+
converter: this,
101+
cause: e,
102+
);
103+
}
104+
}
105+
106+
/// Casts this object to type [T] if possible, otherwise throws a [DogSerializerException].
107+
T expects<T>(dynamic input, DogEngine engine, [String? message]) {
108+
if (input is T) return input;
109+
throw DogSerializerException(
110+
message: message ?? "Expected type $T but got $runtimeType", converter: this);
111+
}
112+
113+
/// Casts this object to type [T] if possible, otherwise returns [defaultValue].
114+
T expectsOr<T>(dynamic input, T defaultValue, DogEngine engine) {
115+
if (input is T) return input;
116+
return defaultValue;
117+
}
118+
}

packages/dogs_flutter/lib/converters/auto.dart

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 47 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1+
import 'package:dogs_core/dogs_converter_utils.dart';
12
import 'package:dogs_core/dogs_core.dart';
23
import 'package:flutter/painting.dart';
34

4-
List<double> _parseDoubleTuple4(dynamic value, {required String typeName}) {
5-
if (value is! List) {
6-
throw DogSerializerException(message: "Invalid $typeName value, expected a list");
7-
}
8-
if (value.length != 4) {
9-
throw DogSerializerException(message: "Invalid $typeName value, expected 4 values");
10-
}
11-
final [a, b, c, d] = value;
12-
if (a is num && b is num && c is num && d is num) {
13-
return [a.toDouble(), b.toDouble(), c.toDouble(), d.toDouble()];
5+
List<double> _parseDoubleTuple4(dynamic value, DogEngine engine, DogConverter converter) {
6+
var list = converter.expects<List>(value, engine);
7+
if (list.length != 4) {
8+
throw DogSerializerException(
9+
message: "Expected list of 4 numeric values",
10+
converter: converter,
11+
);
1412
}
15-
throw DogSerializerException(message: "Invalid $typeName value, expected numeric values");
13+
return [
14+
converter.readAs<double>(list[0], engine),
15+
converter.readAs<double>(list[1], engine),
16+
converter.readAs<double>(list[2], engine),
17+
converter.readAs<double>(list[3], engine),
18+
];
1619
}
1720

1821
@linkSerializer
@@ -21,14 +24,8 @@ class FlutterOffsetConverter extends SimpleDogConverter<Offset> {
2124

2225
@override
2326
Offset deserialize(value, DogEngine engine) {
24-
if (value is Map<String, dynamic>) {
25-
var dx = value["dx"];
26-
var dy = value["dy"];
27-
if (dx is num && dy is num) {
28-
return Offset(dx.toDouble(), dy.toDouble());
29-
}
30-
}
31-
throw DogSerializerException(message: "Invalid offset value", converter: this);
27+
var map = readAsMap(value, engine);
28+
return Offset(map.read<double>("dx"), map.read<double>("dy"));
3229
}
3330

3431
@override
@@ -43,14 +40,8 @@ class FlutterSizeConverter extends SimpleDogConverter<Size> {
4340

4441
@override
4542
Size deserialize(value, DogEngine engine) {
46-
if (value is Map<String, dynamic>) {
47-
var width = value["width"];
48-
var height = value["height"];
49-
if (width is num && height is num) {
50-
return Size(width.toDouble(), height.toDouble());
51-
}
52-
}
53-
throw DogSerializerException(message: "Invalid size value", converter: this);
43+
var map = readAsMap(value, engine);
44+
return Size(map.read<double>("width"), map.read<double>("height"));
5445
}
5546

5647
@override
@@ -65,18 +56,14 @@ class FlutterRectConverter extends SimpleDogConverter<Rect> {
6556

6657
@override
6758
Rect deserialize(value, DogEngine engine) {
68-
return fromValue(value);
59+
final [left, top, right, bottom] = _parseDoubleTuple4(value, engine, this);
60+
return Rect.fromLTRB(left, top, right, bottom);
6961
}
7062

7163
@override
7264
serialize(Rect value, DogEngine engine) {
7365
return [value.left, value.top, value.right, value.bottom];
7466
}
75-
76-
static Rect fromValue(dynamic fieldValue) {
77-
final [left, top, right, bottom] = _parseDoubleTuple4(fieldValue, typeName: "rect");
78-
return Rect.fromLTRB(left, top, right, bottom);
79-
}
8067
}
8168

8269
@linkSerializer
@@ -85,18 +72,14 @@ class FlutterEdgeInsetsConverter extends SimpleDogConverter<EdgeInsets> {
8572

8673
@override
8774
EdgeInsets deserialize(value, DogEngine engine) {
88-
return fromValue(value);
75+
final [left, top, right, bottom] = _parseDoubleTuple4(value, engine, this);
76+
return EdgeInsets.fromLTRB(left, top, right, bottom);
8977
}
9078

9179
@override
9280
serialize(EdgeInsets value, DogEngine engine) {
9381
return [value.left, value.top, value.right, value.bottom];
9482
}
95-
96-
static EdgeInsets fromValue(dynamic fieldValue) {
97-
final [left, top, right, bottom] = _parseDoubleTuple4(fieldValue, typeName: "edge insets");
98-
return EdgeInsets.fromLTRB(left, top, right, bottom);
99-
}
10083
}
10184

10285
@linkSerializer
@@ -130,26 +113,19 @@ class FlutterBorderRadiusConverter extends SimpleDogConverter<BorderRadius> {
130113

131114
@override
132115
BorderRadius deserialize(value, DogEngine engine) {
133-
return fromValue(value);
134-
}
135-
136-
@override
137-
serialize(BorderRadius value, DogEngine engine) {
138-
return [value.topLeft.x, value.topRight.x, value.bottomLeft.x, value.bottomRight.x];
139-
}
140-
141-
static BorderRadius fromValue(dynamic fieldValue) {
142-
final [topLeft, topRight, bottomLeft, bottomRight] = _parseDoubleTuple4(
143-
fieldValue,
144-
typeName: "border radius",
145-
);
116+
final [topLeft, topRight, bottomLeft, bottomRight] = _parseDoubleTuple4(value, engine, this);
146117
return BorderRadius.only(
147118
topLeft: Radius.circular(topLeft),
148119
topRight: Radius.circular(topRight),
149120
bottomLeft: Radius.circular(bottomLeft),
150121
bottomRight: Radius.circular(bottomRight),
151122
);
152123
}
124+
125+
@override
126+
serialize(BorderRadius value, DogEngine engine) {
127+
return [value.topLeft.x, value.topRight.x, value.bottomLeft.x, value.bottomRight.x];
128+
}
153129
}
154130

155131
@linkSerializer
@@ -158,25 +134,30 @@ class FlutterRRectConverter extends SimpleDogConverter<RRect> {
158134

159135
@override
160136
RRect deserialize(value, DogEngine engine) {
161-
if (value is Map<String, dynamic>) {
162-
final rect = FlutterRectConverter.fromValue(value["dimensions"]);
163-
final borderRadius = FlutterBorderRadiusConverter.fromValue(value["radii"]);
164-
return RRect.fromRectAndCorners(
165-
rect,
166-
topLeft: borderRadius.topLeft,
167-
topRight: borderRadius.topRight,
168-
bottomLeft: borderRadius.bottomLeft,
169-
bottomRight: borderRadius.bottomRight,
170-
);
171-
}
172-
throw DogSerializerException(message: "Invalid RRect value", converter: this);
137+
var map = readAsMap(value, engine);
138+
final rect = map.read<Rect>("dimensions");
139+
final borderRadius = map.read<BorderRadius>("radii");
140+
return RRect.fromRectAndCorners(
141+
rect,
142+
topLeft: borderRadius.topLeft,
143+
topRight: borderRadius.topRight,
144+
bottomLeft: borderRadius.bottomLeft,
145+
bottomRight: borderRadius.bottomRight,
146+
);
173147
}
174148

175149
@override
176150
serialize(RRect value, DogEngine engine) {
177151
return <String, dynamic>{
178-
"dimensions": [value.left, value.top, value.right, value.bottom],
179-
"radii": [value.tlRadius.x, value.trRadius.x, value.blRadius.x, value.brRadius.x],
152+
"dimensions": engine.toNative<Rect>(value.outerRect),
153+
"radii": engine.toNative<BorderRadius>(
154+
BorderRadius.only(
155+
topLeft: value.tlRadius,
156+
topRight: value.trRadius,
157+
bottomLeft: value.blRadius,
158+
bottomRight: value.brRadius,
159+
),
160+
),
180161
};
181162
}
182163
}

0 commit comments

Comments
 (0)