Skip to content

Commit b93241a

Browse files
committed
feat: enhance enum serialization with fallback and name overrides
@EnumProperty can now be used to specify a fallback enum constant for invalid string values. Names can be overridden using @PropertyName or the @EnumProperty name field. Refs: #15
1 parent 0c07ece commit b93241a

File tree

6 files changed

+153
-21
lines changed

6 files changed

+153
-21
lines changed

packages/dogs_core/lib/src/converter.dart

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,26 +37,29 @@ abstract class DogConverter<T> extends TypeCapture<T> {
3737
/// Contains structure information about the type being serialized and provides
3838
/// an [OperationMode] mapping for all supported operation modes.
3939
const DogConverter(
40-
{this.struct, this.isAssociated = true, @Deprecated(
40+
{this.struct,
41+
this.isAssociated = true,
42+
@Deprecated(
4143
"This parameter has been removed in favor of the shift towards tree based converters. "
42-
"This value is effectively a no-op, please remove it.") this.keepIterables = false});
44+
"This value is effectively a no-op, please remove it.")
45+
this.keepIterables = false});
4346

4447
/// Returns the operation mode for the given [opmodeType] or null if the
4548
/// converter does not support the given operation mode by default.
46-
OperationMode<T>? resolveOperationMode(DogEngine engine, Type opmodeType) => null;
49+
OperationMode<T>? resolveOperationMode(DogEngine engine, Type opmodeType) =>
50+
null;
4751

4852
/// Generates a [SchemaType] describing the output generated by this converter.
4953
///
5054
/// NOTE: Do not return a shared instance of [SchemaType] as it will be modified by the
5155
/// visitor. For such cases, use [SchemaType.clone] to create a new instance.
52-
SchemaType describeOutput(DogEngine engine, SchemaConfig config) => SchemaType.any;
56+
SchemaType describeOutput(DogEngine engine, SchemaConfig config) =>
57+
SchemaType.any;
5358
}
5459

55-
5660
// TODO: Future replacement for @serializable
5761
/// Configures the dogs structuration of a class or enum.
5862
class Structure extends Serializable implements StructureMetadata {
59-
6063
/// If the type is serializable.
6164
final bool serializable;
6265

@@ -66,7 +69,6 @@ class Structure extends Serializable implements StructureMetadata {
6669

6770
/// See @[serializable].
6871
class Serializable {
69-
7072
/// The name of the type used for serialization.
7173
final String? serialName;
7274

@@ -99,9 +101,7 @@ class SerializableLibrary {
99101
}
100102

101103
/// Marks a class or enum as serializable.
102-
const serializable = Structure(
103-
serializable: true
104-
);
104+
const serializable = Structure(serializable: true);
105105

106106
@internal
107107
class LinkSerializer {
@@ -158,6 +158,22 @@ class PropertySerializer {
158158
const PropertySerializer(this.type);
159159
}
160160

161+
/// Configures how the [GeneratedEnumDogConverter] will serialize this individual
162+
/// enum value. The name can be overridden by specifying a [name]. This enum
163+
/// value can also be marked as the fallback value.
164+
class EnumProperty {
165+
166+
/// The name of the enum value used as a map key in serialization.
167+
final String? name;
168+
169+
/// If this enum value should be used as a fallback value when an invalid
170+
/// value is encountered during deserialization.
171+
final bool fallback;
172+
173+
/// Instantiates a new [EnumProperty] with the given [name] and [fallback].
174+
const EnumProperty({this.name, this.fallback = false});
175+
}
176+
161177
/// Simple converter base that only requires a [serialize] and [deserialize]
162178
/// method. Automatically adds [NativeSerializerMode] as well as a synthetic
163179
/// [DogStructure] with the given [SimpleDogConverter.serialName].

packages/dogs_core/lib/src/converters/enum.dart

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ typedef EnumToString<T> = String Function(T?);
2525
/// A [DogConverter] that allows for the conversion of enum values to and from strings.
2626
abstract class GeneratedEnumDogConverter<T extends Enum> extends DogConverter<T>
2727
with OperationMapMixin<T>, EnumConverter<T> {
28+
29+
GeneratedEnumDogConverter();
30+
31+
GeneratedEnumDogConverter.structured({
32+
required String serialName
33+
}) : super(
34+
isAssociated: true,
35+
struct: DogStructure<T>.synthetic(serialName)
36+
);
37+
2838
/// Function that converts a enum value to a string.
2939
EnumToString<T?> get toStr;
3040

@@ -38,7 +48,16 @@ abstract class GeneratedEnumDogConverter<T extends Enum> extends DogConverter<T>
3848
Map<Type, OperationMode<T> Function()> get modes => {
3949
NativeSerializerMode: () => NativeSerializerMode.create(
4050
serializer: (value, engine) => toStr(value),
41-
deserializer: (value, engine) => fromStr(value)!),
51+
deserializer: (value, engine) {
52+
final v = fromStr(value);
53+
if (v == null) {
54+
throw DogSerializerException(
55+
message: "Value '$value' is not a valid enum value.",
56+
converter: this,
57+
);
58+
}
59+
return v;
60+
}),
4261
};
4362

4463
@override

packages/dogs_generator/lib/analyze/structurize.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ TypeChecker dataclassChecker = TypeChecker.fromRuntime(Dataclass);
101101
TypeChecker mapChecker = TypeChecker.fromRuntime(Map);
102102
TypeChecker beanIgnoreChecker = TypeChecker.fromRuntime(beanIgnore.runtimeType);
103103
TypeChecker serializableChecker = TypeChecker.fromRuntime(Serializable);
104+
TypeChecker enumPropertyChecker = TypeChecker.fromRuntime(EnumProperty);
104105

105106
Future<StructurizeResult> structurizeConstructor(
106107
DartType type,

packages/dogs_generator/lib/builders/converter_builder.dart

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import 'dart:async';
22

33
import 'package:analyzer/dart/element/element.dart';
4-
import 'package:analyzer/dart/element/type.dart';
54
import 'package:build/build.dart';
65
import 'package:code_builder/code_builder.dart';
76
import 'package:collection/collection.dart';
87
import 'package:dogs_core/dogs_core.dart';
9-
108
import 'package:dogs_generator/dogs_generator.dart';
119
import 'package:lyell_gen/lyell_gen.dart';
1210
import 'package:source_gen/source_gen.dart';
@@ -29,20 +27,49 @@ class ConverterBuilder extends DogsAdapter<Serializable> {
2927
SubjectCodeContext codeContext) async {
3028
var emitter = DartEmitter();
3129
var converterName = "${element.name}Converter";
30+
31+
String? fallbackFieldName;
32+
final fieldValueMap =
33+
Map.fromEntries(element.fields.where((e) => e.isEnumConstant).map((e) {
34+
final actual = e.name;
35+
var serializedName = e.name;
36+
37+
final propertyName = propertyNameChecker.firstAnnotationOf(e);
38+
if (propertyName != null) {
39+
serializedName =
40+
propertyName.getField("name")?.toStringValue() ?? serializedName;
41+
}
42+
43+
final enumProperty = enumPropertyChecker.firstAnnotationOf(e);
44+
if (enumProperty != null) {
45+
serializedName =
46+
enumProperty.getField("name")?.toStringValue() ?? serializedName;
47+
if (enumProperty.getField("fallback")?.toBoolValue() ?? false) {
48+
fallbackFieldName = actual;
49+
}
50+
}
51+
52+
return MapEntry(actual, serializedName);
53+
}));
54+
3255
var clazz = Class((builder) {
3356
builder.name = converterName;
3457

3558
builder.extend = Reference(
3659
"$genAlias.GeneratedEnumDogConverter<${codeContext.typeName(element.thisType)}>");
3760

61+
builder.constructors.add(Constructor((builder) => builder
62+
..initializers.add(Code(
63+
"super.structured(serialName: '${codeContext.typeName(element.thisType)}')"))));
64+
3865
builder.methods.add(Method((builder) => builder
3966
..name = "values"
4067
..type = MethodType.getter
4168
..returns = Reference("List<String>")
4269
..annotations.add(CodeExpression(Code("override")))
4370
..lambda = true
4471
..body = Code(
45-
"${codeContext.typeName(element.thisType)}.values.map((e) => e.name).toList()")));
72+
"[${fieldValueMap.values.map((e) => "'${sqsLiteralEscape(e)}'").join(",")}]")));
4673

4774
builder.methods.add(Method((builder) => builder
4875
..name = "toStr"
@@ -51,7 +78,12 @@ class ConverterBuilder extends DogsAdapter<Serializable> {
5178
"$genAlias.EnumToString<${codeContext.typeName(element.thisType)}> ")
5279
..annotations.add(CodeExpression(Code("override")))
5380
..lambda = true
54-
..body = Code("(e) => e!.name")));
81+
..body = Code("""
82+
(e) => switch(e) {
83+
${fieldValueMap.entries.map((e) => "${codeContext.typeName(element.thisType)}.${e.key} => '${sqsLiteralEscape(e.value)}',").join("\n")}
84+
null => throw ArgumentError('Enum value cannot be null'),
85+
}
86+
""")));
5587

5688
builder.methods.add(Method((builder) => builder
5789
..name = "fromStr"
@@ -60,8 +92,15 @@ class ConverterBuilder extends DogsAdapter<Serializable> {
6092
"$genAlias.EnumFromString<${codeContext.typeName(element.thisType)}> ")
6193
..annotations.add(CodeExpression(Code("override")))
6294
..lambda = true
63-
..body = Code(
64-
"(e) => ${codeContext.typeName(element.thisType)}.values.firstWhereOrNullDogs((element) => element.name == e)")));
95+
..body = Code("""
96+
(e) => switch(e) {
97+
${fieldValueMap.entries.map((e) => "'${sqsLiteralEscape(e.value)}' => ${codeContext.typeName(element.thisType)}.${e.key},").join("\n")}
98+
_ => ${switch (fallbackFieldName) {
99+
null => "null",
100+
_ => "${codeContext.typeName(element.thisType)}.$fallbackFieldName"
101+
}}
102+
}
103+
""")));
65104
});
66105
codeContext.codeBuffer.writeln(clazz.accept(emitter));
67106
}
@@ -148,7 +187,8 @@ class ConverterBuilder extends DogsAdapter<Serializable> {
148187
log.severe("""
149188
Generic type variables for models are not supported.
150189
If you wish to use class-level generics, please implement a TreeBaseConverterFactory for your base type.
151-
""".trim());
190+
"""
191+
.trim());
152192
}
153193

154194
var referencedClassName = codeContext.className(element);
@@ -341,7 +381,8 @@ If you wish to use class-level generics, please implement a TreeBaseConverterFac
341381
builder.body = Code(bodyBuilder.toString());
342382
}));
343383

344-
var hasRebuildHook = TypeChecker.fromRuntime(PostRebuildHook).isAssignableFromType(element.thisType);
384+
var hasRebuildHook = TypeChecker.fromRuntime(PostRebuildHook)
385+
.isAssignableFromType(element.thisType);
345386

346387
builder.methods.add(Method((builder) => builder
347388
..name = "build"
@@ -389,8 +430,7 @@ return instance;""")));
389430
..type = MethodType.getter
390431
..returns = Reference("${element.name}\$Copy")
391432
..body = Code("toBuilder()")
392-
..lambda = true
393-
));
433+
..lambda = true));
394434

395435
builder.methods.add(Method((builder) => builder
396436
..name = "toBuilder"

smoke/test0/lib/parts/models.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,33 @@ void testModels() {
4040
FieldExclusionModel.variant0, FieldExclusionModel.variant1);
4141
testSingleModel<ClassExclusionModel>(
4242
ClassExclusionModel.variant0, ClassExclusionModel.variant1);
43+
testSingleModel<CombinedEnumTestModel>(CombinedEnumTestModel.variant0, CombinedEnumTestModel.variant1);
4344

45+
test("Enum Properties", () {
46+
expect(() {
47+
dogs.fromNative<EnumA>("invalid", type: EnumA);
48+
}, throwsException);
49+
50+
final bFallback = dogs.fromNative<EnumB>("invalid");
51+
expect(bFallback, EnumB.a);
52+
53+
final aName = dogs.toNative(EnumA.c);
54+
expect(aName, "c");
55+
expect(EnumA.c, dogs.fromNative<EnumA>(aName));
56+
57+
final enumPropertyNameOverride = dogs.toNative(EnumB.c);
58+
expect(enumPropertyNameOverride, "third");
59+
expect(EnumB.c, dogs.fromNative<EnumB>(enumPropertyNameOverride));
60+
61+
final propertyNameOverride = dogs.toNative(EnumB.b);
62+
expect(propertyNameOverride, "second");
63+
expect(EnumB.b, dogs.fromNative<EnumB>(propertyNameOverride));
64+
65+
final noPropertyNameOverride = dogs.toNative(EnumB.a);
66+
expect(noPropertyNameOverride, "a");
67+
expect(EnumB.a, dogs.fromNative<EnumB>(noPropertyNameOverride));
68+
69+
});
4470

4571
test("Default Values", () {
4672
var defaultValues = DefaultValueModel.variant0();

smoke/test0/lib/special.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,4 +249,34 @@ class ClassExclusionModel with Dataclass<ClassExclusionModel> {
249249
b: null,
250250
);
251251
}
252+
}
253+
254+
@serializable
255+
enum EnumB {
256+
@EnumProperty(fallback: true)
257+
a,
258+
@PropertyName("second")
259+
b,
260+
@EnumProperty(name: "third")
261+
c;
262+
}
263+
264+
265+
@serializable
266+
class CombinedEnumTestModel with Dataclass<CombinedEnumTestModel> {
267+
EnumA enumA;
268+
EnumB enumB;
269+
270+
CombinedEnumTestModel({
271+
required this.enumA,
272+
required this.enumB,
273+
});
274+
275+
factory CombinedEnumTestModel.variant0() {
276+
return CombinedEnumTestModel(enumA: EnumA.a, enumB: EnumB.a);
277+
}
278+
279+
factory CombinedEnumTestModel.variant1() {
280+
return CombinedEnumTestModel(enumA: EnumA.c, enumB: EnumB.c);
281+
}
252282
}

0 commit comments

Comments
 (0)