Skip to content

Commit 4073532

Browse files
authored
[native_assets_cli] Validate conditionally required fields (#2126)
Closes: #1826 This PR adds the final step to the generated syntax validation: conditionally required fields: 1. If target OS is `x`, then require `x` config in code config. 2. If target OS is `windows`, then require `windows` in the c compiler config (more info #1913). 3. If link mode is dynamic library bundled or static library, then require a file in a code asset. We could consider trying to nest the fields under the condition, but that has other downsides: RE 1: Then the OS is no longer an enum usable in the code-asset as OS field. (We could consider this if we remove the OS/arch from code asset outputs. We should be able to do this due to the code config always having a single OS and architecture anyway. #2127) RE 2: That would mean the compiler config would be split over two places. `input.config.code.cCompiler` and `inputconfig.code.windows.cCompiler`. Maybe that's better? Maybe not? RE 3: Treating a group of files in assets would then become `input.assets.code.switch( ... )` instead of simply `input.assets.code.map((a) => a.file)`. Maybe that's okay because we don't often use files in such way anyway? WDYT @mosuem @HosseinYousefi? (I'd probably do any of those refactorings in follow up PRs.)
1 parent 2fc2bab commit 4073532

File tree

10 files changed

+336
-38
lines changed

10 files changed

+336
-38
lines changed

pkgs/json_syntax_generator/lib/src/generator/helper_library.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,18 @@ class JsonReader {
203203
return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
204204
' Expected a $expectedType.';
205205
}
206+
207+
/// Traverses a JSON path, returns `null` if the path cannot be traversed.
208+
Object? tryTraverse(List<String> path) {
209+
Object? json = this.json;
210+
for (final key in path) {
211+
if (json is! Map<String, Object?>) {
212+
return null;
213+
}
214+
json = json[key];
215+
}
216+
return json;
217+
}
206218
}
207219
208220
extension on Map<String, Object?> {

pkgs/json_syntax_generator/lib/src/generator/normal_class_generator.dart

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class $className $extendsString {
3434
buffer.writeln(_generateSetupMethod());
3535
buffer.writeln(_generateAccessors());
3636
buffer.writeln(_generateValidateMethod());
37+
buffer.writeln(_generateExtraValidationMethod());
3738
buffer.writeln(_generateToString());
3839
buffer.writeln('''
3940
}
@@ -244,6 +245,7 @@ class $className $extendsString {
244245
final validateCalls = [
245246
for (final property in classInfo.properties)
246247
'...${property.validateName}()',
248+
if (classInfo.extraValidation.isNotEmpty) '..._validateExtraRules()',
247249
];
248250
final validateCallsString = validateCalls.join(',\n');
249251

@@ -264,6 +266,65 @@ class $className $extendsString {
264266
''';
265267
}
266268

269+
String _generateExtraValidationMethod() {
270+
if (classInfo.extraValidation.isEmpty) return '';
271+
final statements =
272+
classInfo.extraValidation
273+
.map(_generateExtraValidationStatements)
274+
.join()
275+
.trim();
276+
return '''
277+
List<String> _validateExtraRules() {
278+
final result = <String>[];
279+
$statements
280+
return result;
281+
}
282+
''';
283+
}
284+
285+
String _generateExtraValidationStatements(
286+
ConditionallyRequired extraValidationRule,
287+
) {
288+
final path = extraValidationRule.conditionPath;
289+
final values = extraValidationRule.conditionValues;
290+
final requiredPath = extraValidationRule.requiredPath;
291+
final pathString = path.map((e) => "'$e'").join(',');
292+
final traverseExpression = '_reader.tryTraverse([$pathString])';
293+
final String conditionExpression;
294+
if (values.length == 1) {
295+
conditionExpression = "$traverseExpression == '${values.single}'";
296+
} else {
297+
final valuesString = values.map((e) => "'$e'").join(',');
298+
conditionExpression = '[$valuesString].contains($traverseExpression)';
299+
}
300+
if (requiredPath.length == 1) {
301+
final jsonKey = requiredPath.single;
302+
return """
303+
if ($conditionExpression) {
304+
result.addAll(_reader.validate<Object>('$jsonKey'));
305+
}
306+
""";
307+
} else if (requiredPath.length == 2) {
308+
final jsonKey0 = requiredPath[0];
309+
final jsonKey1 = requiredPath[1];
310+
return """
311+
if ($conditionExpression) {
312+
final objectErrors = _reader.validate<Map<String, Object?>?>('$jsonKey0');
313+
result.addAll(objectErrors);
314+
if (objectErrors.isEmpty) {
315+
final jsonValue = _reader.get<Map<String, Object?>?>('$jsonKey0');
316+
if (jsonValue != null) {
317+
final reader = JsonReader(jsonValue, [...path, '$jsonKey0']);
318+
result.addAll(reader.validate<Object>('$jsonKey1'));
319+
}
320+
}
321+
}
322+
""";
323+
} else {
324+
throw UnimplementedError('Different path lengths not implemented yet.');
325+
}
326+
}
327+
267328
String _generateToString() {
268329
final className = classInfo.name;
269330
return '''

pkgs/json_syntax_generator/lib/src/generator/property_generator.dart

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,13 @@ class PropertyGenerator {
8686
final classType = classInfo.name;
8787
final fieldName = property.name;
8888
final validateName = property.validateName;
89-
final required = property.isRequired;
89+
final isNullable = property.type.isNullable;
9090

9191
switch (classInfo) {
9292
case EnumClassInfo():
93-
final dartStringType = StringDartType(isNullable: !required);
93+
final dartStringType = StringDartType(isNullable: isNullable);
9494
final earlyReturn =
95-
required ? '' : 'if(jsonValue == null) return null;';
95+
isNullable ? 'if(jsonValue == null) return null;' : '';
9696

9797
buffer.writeln('''
9898
$dartType get $fieldName {
@@ -114,8 +114,8 @@ List<String> $validateName() => _reader.validate<$dartStringType>('$jsonKey');
114114

115115
case NormalClassInfo():
116116
final earlyReturn =
117-
required ? '' : 'if(jsonValue == null) return null;';
118-
final jsonRead = required ? 'map\$' : 'optionalMap';
117+
isNullable ? 'if(jsonValue == null) return null;' : '';
118+
final jsonRead = isNullable ? 'optionalMap' : r'map$';
119119
buffer.writeln('''
120120
$dartType get $fieldName {
121121
final jsonValue = _reader.$jsonRead('$jsonKey'); $earlyReturn
@@ -130,27 +130,21 @@ set $setterName($dartType value) {
130130
$sortOnKey
131131
}
132132
''');
133-
if (required) {
134-
buffer.writeln('''
135-
List<String> $validateName() {
136-
final mapErrors = _reader.validate<Map<String, Object?>>('$jsonKey');
137-
if (mapErrors.isNotEmpty) {
138-
return mapErrors;
139-
}
140-
return $fieldName.validate();
141-
}
142-
''');
143-
} else {
144-
buffer.writeln('''
133+
final mapType = MapDartType(
134+
valueType: const ObjectDartType(isNullable: true),
135+
isNullable: dartType.isNullable,
136+
);
137+
final questionMark = isNullable ? '?' : '';
138+
final orEmptyList = isNullable ? ' ?? []' : '';
139+
buffer.writeln('''
145140
List<String> $validateName() {
146-
final mapErrors = _reader.validate<Map<String, Object?>?>('$jsonKey');
141+
final mapErrors = _reader.validate<$mapType>('$jsonKey');
147142
if (mapErrors.isNotEmpty) {
148143
return mapErrors;
149144
}
150-
return $fieldName?.validate() ?? [];
145+
return $fieldName$questionMark.validate() $orEmptyList;
151146
}
152147
''');
153-
}
154148
}
155149
}
156150
}
@@ -186,8 +180,8 @@ List<String> $validateName() => _reader.validate<$dartType>('$jsonKey');
186180
String setterName,
187181
String sortOnKey,
188182
) {
189-
if (property.isRequired) {
190-
throw UnimplementedError('Expected an optional property.');
183+
if (!property.type.isNullable) {
184+
throw UnimplementedError('Expected a nullable property.');
191185
}
192186
final fieldName = property.name;
193187
final validateName = property.validateName;
@@ -301,12 +295,12 @@ List<String> $validateName() => _reader.validateOptionalMap('$jsonKey');
301295
final validateName = property.validateName;
302296
final itemType = dartType.itemType;
303297
final typeName = itemType.toString();
304-
final required = property.isRequired;
298+
final isNullable = property.type.isNullable;
305299

306300
switch (itemType) {
307301
case ClassDartType():
308-
if (required) {
309-
throw UnimplementedError('Expected an optional property.');
302+
if (!isNullable) {
303+
throw UnimplementedError('Expected a nullable property.');
310304
}
311305
buffer.writeln('''
312306
$dartType get $fieldName {
@@ -351,9 +345,11 @@ List<String> $validateName() {
351345
case SimpleDartType():
352346
switch (itemType.typeName) {
353347
case 'String':
354-
final jsonRead = required ? 'stringList' : 'optionalStringList';
348+
final jsonRead = isNullable ? 'optionalStringList' : 'stringList';
355349
final jsonValidate =
356-
required ? 'validateStringList' : 'validateOptionalStringList';
350+
isNullable
351+
? 'validateOptionalStringList'
352+
: 'validateStringList';
357353
final setter = setOrRemove(dartType, jsonKey);
358354
buffer.writeln('''
359355
$dartType get $fieldName => _reader.$jsonRead('$jsonKey');
@@ -370,9 +366,9 @@ List<String> $validateName() => _reader.$jsonValidate('$jsonKey');
370366
throw UnimplementedError(itemType.toString());
371367
}
372368
case UriDartType():
373-
final jsonRead = required ? 'pathList' : 'optionalPathList';
369+
final jsonRead = isNullable ? 'optionalPathList' : 'pathList';
374370
final jsonValidate =
375-
required ? 'validatePathList' : 'validateOptionalPathList';
371+
isNullable ? 'validateOptionalPathList' : 'validatePathList';
376372
final setter = setOrRemove(dartType, jsonKey, '.toJson()');
377373
buffer.writeln('''
378374
$dartType get $fieldName => _reader.$jsonRead('$jsonKey');
@@ -399,9 +395,9 @@ List<String> $validateName() => _reader.$jsonValidate('$jsonKey');
399395
) {
400396
final fieldName = property.name;
401397
final validateName = property.validateName;
402-
final required = property.isRequired;
403-
final jsonRead = required ? r'path$' : 'optionalPath';
404-
final jsonValidate = required ? r'validatePath' : 'validateOptionalPath';
398+
final isNullable = property.type.isNullable;
399+
final jsonRead = isNullable ? 'optionalPath' : r'path$';
400+
final jsonValidate = isNullable ? 'validateOptionalPath' : 'validatePath';
405401
final setter = setOrRemove(dartType, jsonKey, '.toFilePath()');
406402
buffer.writeln('''
407403
$dartType get $fieldName => _reader.$jsonRead('$jsonKey');

pkgs/json_syntax_generator/lib/src/model/class_info.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,15 @@ class NormalClassInfo extends ClassInfo {
4343
bool get isTaggedUnion =>
4444
taggedUnionProperty != null || taggedUnionValue != null;
4545

46+
final List<ConditionallyRequired> extraValidation;
47+
4648
NormalClassInfo({
4749
required super.name,
4850
this.superclass,
4951
required this.properties,
5052
this.taggedUnionProperty,
5153
this.taggedUnionValue,
54+
this.extraValidation = const [],
5255
}) : super() {
5356
superclass?.subclasses.add(this);
5457
if (taggedUnionValue != null) {
@@ -61,6 +64,9 @@ class NormalClassInfo extends ClassInfo {
6164
final propertiesString = properties
6265
.map((p) => indentLines(p.toString(), level: 2))
6366
.join(',\n');
67+
final extraValidationString = extraValidation
68+
.map((p) => indentLines(p.toString(), level: 2))
69+
.join('\n');
6470
return '''
6571
$runtimeType(
6672
name: $name,
@@ -71,10 +77,39 @@ $propertiesString
7177
],
7278
taggedUnionProperty: $taggedUnionProperty,
7379
taggedUnionValue: $taggedUnionValue,
80+
extraValidation: [
81+
$extraValidationString
82+
],
7483
)''';
7584
}
7685
}
7786

87+
/// The property [requiredPath] is required if some [conditionPath] has a value
88+
/// in [conditionValues].
89+
///
90+
/// This class is special cased to cover the uses cases seen so far. If
91+
/// different types of conditionals are needed, this class should probably be
92+
/// extended to cover some arbitrary expression.
93+
class ConditionallyRequired {
94+
final List<String> conditionPath;
95+
final List<String> conditionValues;
96+
final List<String> requiredPath;
97+
98+
const ConditionallyRequired({
99+
required this.conditionPath,
100+
required this.conditionValues,
101+
required this.requiredPath,
102+
});
103+
104+
@override
105+
String toString() => '''
106+
ConditionallyRequired(
107+
path: $conditionPath,
108+
values: $conditionValues,
109+
required: $requiredPath,
110+
)''';
111+
}
112+
78113
class EnumClassInfo extends ClassInfo {
79114
final List<EnumValue> enumValues;
80115
final bool isOpen;

pkgs/json_syntax_generator/lib/src/model/property_info.dart

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ class PropertyInfo {
3535
/// meal. See [SchemaAnalyzer.publicSetters].
3636
final bool setterPrivate;
3737

38-
bool get isRequired => !type.isNullable;
39-
4038
PropertyInfo({
4139
required this.name,
4240
required this.jsonKey,
@@ -53,7 +51,6 @@ PropertyInfo(
5351
type: $type,
5452
isOverride: $isOverride,
5553
setterPrivate: $setterPrivate,
56-
isRequired: $isRequired,
5754
)''';
5855
}
5956

0 commit comments

Comments
 (0)