Skip to content

Commit 5b5f0c6

Browse files
feat: Global default enum value to prevent breaking the client (serverpod#3341)
1 parent a6f77e6 commit 5b5f0c6

File tree

16 files changed

+2726
-2407
lines changed

16 files changed

+2726
-2407
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
2+
/* To generate run: "serverpod generate" */
3+
4+
// ignore_for_file: implementation_imports
5+
// ignore_for_file: library_private_types_in_public_api
6+
// ignore_for_file: non_constant_identifier_names
7+
// ignore_for_file: public_member_api_docs
8+
// ignore_for_file: type_literal_in_constant_pattern
9+
// ignore_for_file: use_super_parameters
10+
11+
// ignore_for_file: no_leading_underscores_for_library_prefixes
12+
import 'package:serverpod_client/serverpod_client.dart' as _i1;
13+
14+
enum DefaultValueEnum implements _i1.SerializableModel {
15+
value1,
16+
value2;
17+
18+
static DefaultValueEnum fromJson(int index) {
19+
switch (index) {
20+
case 0:
21+
return DefaultValueEnum.value1;
22+
case 1:
23+
return DefaultValueEnum.value2;
24+
default:
25+
return DefaultValueEnum.value2;
26+
}
27+
}
28+
29+
@override
30+
int toJson() => index;
31+
@override
32+
String toString() => name;
33+
}

tests/serverpod_test_client/lib/src/protocol/protocol.dart

+1,145-1,131
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
2+
/* To generate run: "serverpod generate" */
3+
4+
// ignore_for_file: implementation_imports
5+
// ignore_for_file: library_private_types_in_public_api
6+
// ignore_for_file: non_constant_identifier_names
7+
// ignore_for_file: public_member_api_docs
8+
// ignore_for_file: type_literal_in_constant_pattern
9+
// ignore_for_file: use_super_parameters
10+
11+
// ignore_for_file: no_leading_underscores_for_library_prefixes
12+
import 'package:serverpod/serverpod.dart' as _i1;
13+
14+
enum DefaultValueEnum implements _i1.SerializableModel {
15+
value1,
16+
value2;
17+
18+
static DefaultValueEnum fromJson(int index) {
19+
switch (index) {
20+
case 0:
21+
return DefaultValueEnum.value1;
22+
case 1:
23+
return DefaultValueEnum.value2;
24+
default:
25+
return DefaultValueEnum.value2;
26+
}
27+
}
28+
29+
@override
30+
int toJson() => index;
31+
@override
32+
String toString() => name;
33+
}

tests/serverpod_test_server/lib/src/generated/protocol.dart

+1,282-1,268
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
enum: DefaultValueEnum
2+
default: value2
3+
values:
4+
- value1
5+
- value2

tests/serverpod_test_server/test_integration/database_operations/default/enum/enum_default_test.dart

+37
Original file line numberDiff line numberDiff line change
@@ -139,5 +139,42 @@ void main() async {
139139
);
140140
},
141141
);
142+
143+
group('Given a class with "default" enum fields,', () {
144+
test(
145+
'when deserializing an invalid index for DefaultValueEnum, it should default to value2',
146+
() async {
147+
var object = DefaultValueEnum.fromJson(-1);
148+
expect(DefaultValueEnum.values.length, 2);
149+
expect(object, DefaultValueEnum.value2);
150+
},
151+
);
152+
153+
test(
154+
'when deserializing an invalid index for ByIndexEnum, it should throw an ArgumentError',
155+
() async {
156+
expect(
157+
() => ByIndexEnum.fromJson(-1),
158+
throwsA(predicate((e) =>
159+
e is ArgumentError &&
160+
e.message ==
161+
'Value "-1" cannot be converted to "ByIndexEnum"')),
162+
);
163+
},
164+
);
165+
166+
test(
167+
'when deserializing an invalid name for ByNameEnum, it should throw an ArgumentError',
168+
() async {
169+
expect(
170+
() => ByNameEnum.fromJson('Invalid'),
171+
throwsA(predicate((e) =>
172+
e is ArgumentError &&
173+
e.message ==
174+
'Value "Invalid" cannot be converted to "ByNameEnum"')),
175+
);
176+
},
177+
);
178+
});
142179
});
143180
}

tools/serverpod_cli/lib/src/analyzer/models/definitions.dart

+4
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,9 @@ class EnumDefinition extends SerializableModelDefinition {
342342
/// The type of serialization this enum should use.
343343
final EnumSerialization serialized;
344344

345+
/// The default value of the enum when parsing of the enum fails.
346+
final ProtocolEnumValueDefinition? defaultValue;
347+
345348
/// All the values of the enum.
346349
/// This also contains possible documentation for them.
347350
List<ProtocolEnumValueDefinition> values;
@@ -355,6 +358,7 @@ class EnumDefinition extends SerializableModelDefinition {
355358
required super.sourceFileName,
356359
required super.className,
357360
required this.serialized,
361+
required this.defaultValue,
358362
required this.values,
359363
required super.serverOnly,
360364
required super.type,

tools/serverpod_cli/lib/src/analyzer/models/model_parser/model_parser.dart

+12
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ class ModelParser {
181181
'${protocolSource.moduleAlias}:$className',
182182
extraClasses: [],
183183
);
184+
var defaultEnumDefinitionValue =
185+
_parseEnumDefaultValue(documentContents, values);
184186

185187
var enumDef = EnumDefinition(
186188
fileName: outFileName,
@@ -189,6 +191,7 @@ class ModelParser {
189191
values: values,
190192
serialized: serializeAs,
191193
documentation: enumDocumentation,
194+
defaultValue: defaultEnumDefinitionValue,
192195
subDirParts: protocolSource.subDirPathParts,
193196
serverOnly: serverOnly,
194197
type: enumType,
@@ -605,6 +608,15 @@ class ModelParser {
605608
return nodeValue is bool ? nodeValue : false;
606609
}
607610

611+
static ProtocolEnumValueDefinition? _parseEnumDefaultValue(
612+
YamlMap documentContents,
613+
List<ProtocolEnumValueDefinition> values,
614+
) {
615+
final defaultValue =
616+
_parseDefaultValue(documentContents, Keyword.defaultKey);
617+
return values.where((value) => value.name == defaultValue).firstOrNull;
618+
}
619+
608620
static List<ProtocolEnumValueDefinition> _parseEnumValues(
609621
YamlMap documentContents,
610622
YamlDocumentationExtractor docsExtractor,

tools/serverpod_cli/lib/src/analyzer/models/validation/restrictions.dart

+26
Original file line numberDiff line numberDiff line change
@@ -1233,6 +1233,32 @@ class Restrictions {
12331233
return [];
12341234
}
12351235

1236+
List<SourceSpanSeverityException> validateEnumDefaultValue(
1237+
String parentNodeName,
1238+
dynamic content,
1239+
SourceSpan? span,
1240+
) {
1241+
if (content is! String) {
1242+
return [
1243+
SourceSpanSeverityException(
1244+
'The "default" property must be a String.',
1245+
span,
1246+
)
1247+
];
1248+
}
1249+
1250+
var definition = documentDefinition;
1251+
if (definition is! EnumDefinition) return [];
1252+
final values = definition.values;
1253+
if (values.any((value) => value.name == content)) return [];
1254+
return [
1255+
SourceSpanSeverityException(
1256+
'"$content" is not a valid default value. Allowed values are: ${values.map((e) => e.name).join(', ')}.',
1257+
span,
1258+
)
1259+
];
1260+
}
1261+
12361262
List<SourceSpanSeverityException> validateEnumValues(
12371263
String parentNodeName,
12381264
dynamic content,

tools/serverpod_cli/lib/src/analyzer/models/yaml_definitions/enum_yaml_definition.dart

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ class EnumYamlDefinition {
1919
valueRestriction:
2020
EnumValueRestriction(enums: EnumSerialization.values).validate,
2121
),
22+
ValidateNode(
23+
Keyword.defaultKey,
24+
valueRestriction: restrictions.validateEnumDefaultValue,
25+
),
2226
ValidateNode(
2327
Keyword.serverOnly,
2428
valueRestriction: BooleanValueRestriction().validate,

tools/serverpod_cli/lib/src/generator/dart/library_generators/model_library_generator.dart

+14-6
Original file line numberDiff line numberDiff line change
@@ -2393,9 +2393,7 @@ class SerializableModelLibraryGenerator {
23932393
Code(
23942394
'case $i: return ${enumDefinition.className}.${enumDefinition.values[i].name};',
23952395
),
2396-
Code(
2397-
'default: throw ArgumentError(\'Value "\$index" cannot be converted to "${enumDefinition.className}"\');',
2398-
),
2396+
_buildDefaultSwitchCase(enumDefinition, 'index'),
23992397
const Code('}'),
24002398
]))
24012399
.build()),
@@ -2429,9 +2427,7 @@ class SerializableModelLibraryGenerator {
24292427
Code(
24302428
"case '${value.name}': return ${enumDefinition.className}.${value.name};",
24312429
),
2432-
Code(
2433-
'default: throw ArgumentError(\'Value "\$name" cannot be converted to "${enumDefinition.className}"\');',
2434-
),
2430+
_buildDefaultSwitchCase(enumDefinition, 'name'),
24352431
const Code('}'),
24362432
]))
24372433
.build()),
@@ -2476,6 +2472,18 @@ class SerializableModelLibraryGenerator {
24762472

24772473
return refer(field.name);
24782474
}
2475+
2476+
Code _buildDefaultSwitchCase(
2477+
EnumDefinition enumDefinition, String valueFieldName) {
2478+
if (enumDefinition.defaultValue == null) {
2479+
return Code(
2480+
'default: throw ArgumentError(\'Value "\$$valueFieldName" cannot be converted to "${enumDefinition.className}"\');',
2481+
);
2482+
}
2483+
return Code(
2484+
'default: return ${enumDefinition.className}.${enumDefinition.defaultValue!.name};',
2485+
);
2486+
}
24792487
}
24802488

24812489
class SimpleData {}

tools/serverpod_cli/test/analyzer/models/stateful_analyzer/model_validation/class_type_test.dart

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:serverpod_cli/analyzer.dart';
22
import 'package:serverpod_cli/src/analyzer/models/stateful_analyzer.dart';
33
import 'package:serverpod_cli/src/generator/code_generation_collector.dart';
4+
import 'package:serverpod_service_client/serverpod_service_client.dart';
45
import 'package:test/test.dart';
56

67
import '../../../../test_util/builders/generator_config_builder.dart';
@@ -54,6 +55,8 @@ void main() {
5455

5556
expect(model.type.className, model.className);
5657
expect(model.type.enumDefinition, model);
58+
expect(model.defaultValue, isNull);
59+
expect(model.serialized, EnumSerialization.byIndex);
5760
});
5861

5962
test(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import 'package:serverpod_cli/src/analyzer/models/definitions.dart';
2+
import 'package:serverpod_cli/src/analyzer/models/stateful_analyzer.dart';
3+
import 'package:serverpod_cli/src/generator/code_generation_collector.dart';
4+
import 'package:test/test.dart';
5+
6+
import '../../../../test_util/builders/generator_config_builder.dart';
7+
import '../../../../test_util/builders/model_source_builder.dart';
8+
9+
void main() {
10+
var config = GeneratorConfigBuilder().build();
11+
12+
group(
13+
'Given a valid enum definition with default value set to valid value when validating',
14+
() {
15+
late CodeGenerationCollector collector;
16+
late EnumDefinition definition;
17+
setUp(() {
18+
var modelSources = [
19+
ModelSourceBuilder().withYaml(
20+
'''
21+
enum: ExampleEnum
22+
default: first
23+
values:
24+
- first
25+
- second
26+
''',
27+
).build()
28+
];
29+
30+
collector = CodeGenerationCollector();
31+
var analyzer =
32+
StatefulAnalyzer(config, modelSources, onErrorsCollector(collector));
33+
34+
var definitions = analyzer.validateAll();
35+
definition = definitions.first as EnumDefinition;
36+
});
37+
38+
test('then no errors are collected', () {
39+
expect(collector.errors, isEmpty);
40+
});
41+
42+
test('then defaultValue is set to valid value.', () {
43+
expect(definition.defaultValue, isNotNull);
44+
expect(definition.defaultValue!.name, definition.values.first.name);
45+
});
46+
});
47+
48+
group(
49+
'Given a valid enum definition with default value set to an invalid String value when validating',
50+
() {
51+
late CodeGenerationCollector collector;
52+
53+
setUp(() {
54+
var modelSources = [
55+
ModelSourceBuilder().withYaml(
56+
'''
57+
enum: ExampleEnum
58+
default: Invalid
59+
values:
60+
- first
61+
- second
62+
''',
63+
).build()
64+
];
65+
66+
collector = CodeGenerationCollector();
67+
var analyzer =
68+
StatefulAnalyzer(config, modelSources, onErrorsCollector(collector));
69+
70+
analyzer.validateAll();
71+
});
72+
73+
test('then 1 error is collected', () {
74+
expect(collector.errors, isNotEmpty);
75+
expect(collector.errors.length, 1);
76+
expect(
77+
collector.errors.first.message,
78+
'"Invalid" is not a valid default value. Allowed values are: first, second.',
79+
);
80+
});
81+
});
82+
83+
group(
84+
'Given a valid enum definition with default value set to and invalid int value when validating',
85+
() {
86+
late CodeGenerationCollector collector;
87+
88+
setUp(() {
89+
var modelSources = [
90+
ModelSourceBuilder().withYaml(
91+
'''
92+
enum: ExampleEnum
93+
default: 0
94+
values:
95+
- first
96+
- second
97+
''',
98+
).build()
99+
];
100+
101+
collector = CodeGenerationCollector();
102+
var analyzer =
103+
StatefulAnalyzer(config, modelSources, onErrorsCollector(collector));
104+
105+
analyzer.validateAll();
106+
});
107+
108+
test('then 1 error is collected', () {
109+
expect(collector.errors, isNotEmpty);
110+
expect(collector.errors.length, 1);
111+
expect(
112+
collector.errors.first.message,
113+
'The "default" property must be a String.',
114+
);
115+
});
116+
});
117+
}

tools/serverpod_cli/test/analyzer/models/stateful_analyzer/model_validation/extra_properties_test.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ void main() {
442442
var error = collector.errors.first;
443443
expect(
444444
error.message,
445-
'The "table" property is not allowed for enum type. Valid keys are {enum, serialized, serverOnly, values}.',
445+
'The "table" property is not allowed for enum type. Valid keys are {enum, serialized, default, serverOnly, values}.',
446446
);
447447
},
448448
);

0 commit comments

Comments
 (0)