Skip to content

Commit ab00eab

Browse files
committed
feat: add enumeration support and corresponding Flutter bindings
1 parent 968856c commit ab00eab

File tree

7 files changed

+249
-14
lines changed

7 files changed

+249
-14
lines changed

packages/dogs/lib/dogs_schema.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ SchemaType map(SchemaType itemType) {
2828
return SchemaType.map(itemType);
2929
}
3030

31+
SchemaType enumeration(List<String> values) {
32+
return SchemaType.string.property(SchemaProperties.$enum, values);
33+
}
34+
35+
3136
extension SchemaTypeExtension on SchemaType {
3237
SchemaType array() => SchemaType.array(this);
3338

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

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ abstract class GeneratedEnumDogConverter<T extends Enum> extends DogConverter<T>
5858
/// Mixin that exposes a public api surface for enum converters.
5959
/// Primarily intended to be used by opmode factories for converter tree
6060
/// introspection.
61-
mixin EnumConverter<T extends Enum> on DogConverter<T> {
61+
mixin EnumConverter<T> on DogConverter<T> {
6262
/// All possible enum values.
6363
List<String> get values;
6464

@@ -68,3 +68,49 @@ mixin EnumConverter<T extends Enum> on DogConverter<T> {
6868
/// Converts a enum value to a string.
6969
String valueToString(T? value);
7070
}
71+
72+
73+
class RuntimeEnumConverter extends SimpleDogConverter<String> with EnumConverter<String> {
74+
@override
75+
final List<String> values;
76+
77+
RuntimeEnumConverter(this.values, String serialName) : super(serialName: serialName);
78+
79+
@override
80+
String deserialize(value, DogEngine engine) {
81+
if (!values.contains(value)) {
82+
throw DogSerializerException(
83+
message: "Value '$value' is not a valid enum value. Valid values are: $values",
84+
converter: this
85+
);
86+
}
87+
return value as String;
88+
}
89+
90+
@override
91+
serialize(String value, DogEngine engine) {
92+
return value;
93+
}
94+
95+
@override
96+
String? valueFromString(String value) {
97+
if (value == "null") return null;
98+
if (values.contains(value)) {
99+
return value;
100+
}
101+
throw ArgumentError("Value '$value' is not a valid enum value. Valid values are: $values");
102+
}
103+
104+
@override
105+
String valueToString(String? value) {
106+
if (value == null) return "null";
107+
return value;
108+
}
109+
110+
@override
111+
SchemaType describeOutput(DogEngine engine, SchemaConfig config) {
112+
final type = SchemaType.string;
113+
type[SchemaProperties.$enum] = values;
114+
return type;
115+
}
116+
}

packages/dogs/lib/src/schema/materialize.dart

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,13 +174,25 @@ class DogsMaterializer {
174174
isPolymorphic = true;
175175
return QualifiedTypeTree.terminal<dynamic>();
176176
});
177+
final annotations = <StructureMetadata>[];
178+
if (isPolymorphic) {
179+
annotations.add(polymorphic);
180+
}
181+
182+
final enumProperty = field.type[SchemaProperties.$enum] as List<String>?;
183+
if (enumProperty != null) {
184+
annotations.add(UseConverterInstance(
185+
RuntimeEnumConverter(enumProperty, "${field.name}Enum")
186+
));
187+
}
188+
177189
final structureField = DogStructureField(
178190
materializedTypeTree,
179191
null,
180192
field.name,
181193
field.type.nullable,
182194
true,
183-
isPolymorphic ? [polymorphic] : [],
195+
annotations
184196
);
185197
return structureField;
186198
}

packages/dogs/test/dogs_schema.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,36 @@ void main() {
6363
});
6464
});
6565

66+
test("Enum Properties", () {
67+
final base = z.object({
68+
"first": z.enumeration(["A", "B", "C"]),
69+
});
70+
expect(base, doesReserialize);
71+
final case0 = {
72+
"first": "A",
73+
};
74+
final case1 = {
75+
"first": "B",
76+
};
77+
final case2 = {
78+
"first": "C",
79+
};
80+
final case3 = {
81+
"first": "D", // Invalid case
82+
};
83+
84+
final serializer = DogEngine().materialize(base);
85+
final encoded0 = serializer.toJson(case0);
86+
final decoded0 = serializer.fromJson(encoded0);
87+
expect(decoded0, unorderedDeepEquals(case0));
88+
final encoded1 = serializer.toJson(case1);
89+
final decoded1 = serializer.fromJson(encoded1);
90+
expect(decoded1, unorderedDeepEquals(case1));
91+
final decoded2 = serializer.fromNative(case2);
92+
expect(decoded2, unorderedDeepEquals(case2));
93+
expect(() => serializer.fromNative(case3), throwsA(isA<DogException>()));
94+
});
95+
6696
test("Unroll Nested Schema", () {
6797
final base = z.object({
6898
"name": z.string(),

packages/dogs_flutter/example/lib/main.dart

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,11 @@ class _TestFormState extends State<TestForm> {
6161
"subfield1": z.string(),
6262
"subfield2": z.integer()
6363
}),
64-
"array": z.string().array().min(3).max(5),
65-
"objectArray": z.object({
66-
"name": z.string(),
67-
"value": z.integer().positive(),
68-
}).array()
64+
"enum": z.enumeration([
65+
"option1",
66+
"option2",
67+
"option3",
68+
])
6969
}),
7070
);
7171

@@ -110,11 +110,7 @@ class _TestFormState extends State<TestForm> {
110110
"surname": "Doe",
111111
"age": 21,
112112
"subschema": {"subfield1": "value1", "subfield2": 42},
113-
"array": ["item1", "item2", "item3"],
114-
"objectArray": [
115-
{"name": "Item1", "value": 1},
116-
{"name": "Item2", "value": 2},
117-
],
113+
"enum": "option2",
118114
});
119115

120116
setState(() {
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import 'package:dogs_flutter/dogs_flutter.dart';
2+
import 'package:flutter/material.dart';
3+
4+
class EnumFlutterBinder extends FlutterWidgetBinder<String>
5+
with TypeCaptureMixin<String>
6+
implements StructureMetadata {
7+
final EnumConverter converter;
8+
9+
const EnumFlutterBinder(this.converter);
10+
11+
@override
12+
Widget buildBindingField(
13+
BuildContext context,
14+
FieldBindingController<String> controller,
15+
) {
16+
return EnumBindingFieldWidget(
17+
key: Key(controller.fieldName),
18+
controller: controller as EnumBindingFieldController,
19+
);
20+
}
21+
22+
@override
23+
FieldBindingController<String> createBindingController(
24+
FieldBindingParent parent,
25+
FieldBindingContext<String> context,
26+
) {
27+
return EnumBindingFieldController(parent, this, context, converter);
28+
}
29+
30+
@override
31+
void initialise(DogEngine engine) {}
32+
33+
@override
34+
bool operator ==(Object other) =>
35+
identical(this, other) ||
36+
other is EnumFlutterBinder &&
37+
runtimeType == other.runtimeType &&
38+
converter == other.converter;
39+
40+
@override
41+
int get hashCode => converter.hashCode;
42+
}
43+
44+
class EnumBindingFieldController extends FieldBindingController<String> {
45+
final FocusNode focusNode = FocusNode();
46+
String? value;
47+
final EnumConverter converter;
48+
49+
EnumBindingFieldController(
50+
super.parent,
51+
super.binder,
52+
super.bindingContext,
53+
this.converter,
54+
) {
55+
focusNode.addListener(_onFocusChanged);
56+
}
57+
58+
@override
59+
void dispose() {
60+
super.dispose();
61+
focusNode.dispose();
62+
}
63+
64+
void _onFocusChanged() {
65+
if (!focusNode.hasFocus) {
66+
performValidation(ValidationTrigger.onUnfocus);
67+
}
68+
}
69+
70+
@override
71+
String? getValue() {
72+
return value;
73+
}
74+
75+
@override
76+
void setValue(String? value) {
77+
this.value = value;
78+
notifyListeners();
79+
}
80+
}
81+
82+
class EnumBindingFieldWidget extends StatelessWidget {
83+
final EnumBindingFieldController controller;
84+
85+
const EnumBindingFieldWidget({super.key, required this.controller});
86+
87+
@override
88+
Widget build(BuildContext context) {
89+
final theme = BindingTheme.of(context);
90+
return ValueListenableBuilder(
91+
valueListenable: controller.errorListenable,
92+
builder: (context, error, _) {
93+
final outerDecoration = theme.style
94+
.buildMaterialDecoration(
95+
context,
96+
controller,
97+
includeLabel: false,
98+
includeHint: false,
99+
includeHelper: false,
100+
)
101+
.copyWith(
102+
errorText: theme.toErrorText(error),
103+
border: InputBorder.none,
104+
isDense: true,
105+
);
106+
return InputDecorator(
107+
decoration: outerDecoration,
108+
child: ListenableBuilder(
109+
listenable: controller,
110+
builder:
111+
(context, child) => DropdownButton<String>(
112+
value: controller.value,
113+
items:
114+
controller.converter.values
115+
.map(
116+
(e) => DropdownMenuItem(value: e, child: Text(e)),
117+
)
118+
.toList(),
119+
isExpanded: true,
120+
hint: theme.style.buildMaterialLabelText(context),
121+
onChanged: (String? value) {
122+
controller.setValue(value);
123+
},
124+
),
125+
),
126+
);
127+
},
128+
);
129+
}
130+
}
131+
132+
class EnumAutoFactory extends OperationModeFactory<FlutterWidgetBinder> {
133+
@override
134+
FlutterWidgetBinder? forConverter(
135+
DogConverter<dynamic> converter,
136+
DogEngine engine,
137+
) {
138+
if (converter is EnumConverter) {
139+
return EnumFlutterBinder(converter);
140+
}
141+
return null;
142+
}
143+
}

packages/dogs_flutter/lib/dogs_flutter.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
library;
22

3+
import 'package:dogs_flutter/databinding/bindings/enum.dart';
4+
35
import 'dogs_flutter.dart';
46
import 'dogs.g.dart';
57

@@ -44,12 +46,13 @@ export 'schema/custom_tags.dart';
4446
export 'schema/binding_style_contributor.dart';
4547

4648
final defaultFactories = OperationModeFactory.compose<FlutterWidgetBinder>([
49+
ListAutoFactory(),
50+
EnumAutoFactory(),
51+
NestedStructureAutoFactory(),
4752
OperationModeFactory.typeSingleton<String, FlutterWidgetBinder>(StringFlutterBinder()),
4853
OperationModeFactory.typeSingleton<int, FlutterWidgetBinder>(IntFlutterBinder()),
4954
OperationModeFactory.typeSingleton<double, FlutterWidgetBinder>(DoubleFlutterBinder()),
5055
OperationModeFactory.typeSingleton<bool, FlutterWidgetBinder>(BoolFlutterBinder()),
51-
ListAutoFactory(),
52-
NestedStructureAutoFactory()
5356
]);
5457

5558
void configureDogsFlutter({

0 commit comments

Comments
 (0)