Skip to content

[native_assets_cli] Validate conditionally required fields #2126

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions pkgs/json_syntax_generator/lib/src/generator/helper_library.dart
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,18 @@ class JsonReader {
return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
' Expected a $expectedType.';
}

/// Traverses a JSON path, returns `null` if the path cannot be traversed.
Object? tryTraverse(List<String> path) {
Object? json = this.json;
for (final key in path) {
if (json is! Map<String, Object?>) {
return null;
}
json = json[key];
}
return json;
}
}

extension on Map<String, Object?> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class $className $extendsString {
buffer.writeln(_generateSetupMethod());
buffer.writeln(_generateAccessors());
buffer.writeln(_generateValidateMethod());
buffer.writeln(_generateExtraValidationMethod());
buffer.writeln(_generateToString());
buffer.writeln('''
}
Expand Down Expand Up @@ -244,6 +245,7 @@ class $className $extendsString {
final validateCalls = [
for (final property in classInfo.properties)
'...${property.validateName}()',
if (classInfo.extraValidation.isNotEmpty) '..._validateExtraRules()',
];
final validateCallsString = validateCalls.join(',\n');

Expand All @@ -264,6 +266,65 @@ class $className $extendsString {
''';
}

String _generateExtraValidationMethod() {
if (classInfo.extraValidation.isEmpty) return '';
final statements =
classInfo.extraValidation
.map(_generateExtraValidationStatements)
.join()
.trim();
return '''
List<String> _validateExtraRules() {
final result = <String>[];
$statements
return result;
}
''';
}

String _generateExtraValidationStatements(
ConditionallyRequired extraValidationRule,
) {
final path = extraValidationRule.conditionPath;
final values = extraValidationRule.conditionValues;
final requiredPath = extraValidationRule.requiredPath;
final pathString = path.map((e) => "'$e'").join(',');
final traverseExpression = '_reader.tryTraverse([$pathString])';
final String conditionExpression;
if (values.length == 1) {
conditionExpression = "$traverseExpression == '${values.single}'";
} else {
final valuesString = values.map((e) => "'$e'").join(',');
conditionExpression = '[$valuesString].contains($traverseExpression)';
}
if (requiredPath.length == 1) {
final jsonKey = requiredPath.single;
return """
if ($conditionExpression) {
result.addAll(_reader.validate<Object>('$jsonKey'));
}
""";
} else if (requiredPath.length == 2) {
final jsonKey0 = requiredPath[0];
final jsonKey1 = requiredPath[1];
return """
if ($conditionExpression) {
final objectErrors = _reader.validate<Map<String, Object?>?>('$jsonKey0');
result.addAll(objectErrors);
if (objectErrors.isEmpty) {
final jsonValue = _reader.get<Map<String, Object?>?>('$jsonKey0');
if (jsonValue != null) {
final reader = JsonReader(jsonValue, [...path, '$jsonKey0']);
result.addAll(reader.validate<Object>('$jsonKey1'));
}
}
}
""";
} else {
throw UnimplementedError('Different path lengths not implemented yet.');
}
}

String _generateToString() {
final className = classInfo.name;
return '''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,13 @@ class PropertyGenerator {
final classType = classInfo.name;
final fieldName = property.name;
final validateName = property.validateName;
final required = property.isRequired;
final isNullable = property.type.isNullable;

switch (classInfo) {
case EnumClassInfo():
final dartStringType = StringDartType(isNullable: !required);
final dartStringType = StringDartType(isNullable: isNullable);
final earlyReturn =
required ? '' : 'if(jsonValue == null) return null;';
isNullable ? 'if(jsonValue == null) return null;' : '';

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

case NormalClassInfo():
final earlyReturn =
required ? '' : 'if(jsonValue == null) return null;';
final jsonRead = required ? 'map\$' : 'optionalMap';
isNullable ? 'if(jsonValue == null) return null;' : '';
final jsonRead = isNullable ? 'optionalMap' : r'map$';
buffer.writeln('''
$dartType get $fieldName {
final jsonValue = _reader.$jsonRead('$jsonKey'); $earlyReturn
Expand All @@ -130,27 +130,21 @@ set $setterName($dartType value) {
$sortOnKey
}
''');
if (required) {
buffer.writeln('''
List<String> $validateName() {
final mapErrors = _reader.validate<Map<String, Object?>>('$jsonKey');
if (mapErrors.isNotEmpty) {
return mapErrors;
}
return $fieldName.validate();
}
''');
} else {
buffer.writeln('''
final mapType = MapDartType(
valueType: const ObjectDartType(isNullable: true),
isNullable: dartType.isNullable,
);
final questionMark = isNullable ? '?' : '';
final orEmptyList = isNullable ? ' ?? []' : '';
buffer.writeln('''
List<String> $validateName() {
final mapErrors = _reader.validate<Map<String, Object?>?>('$jsonKey');
final mapErrors = _reader.validate<$mapType>('$jsonKey');
if (mapErrors.isNotEmpty) {
return mapErrors;
}
return $fieldName?.validate() ?? [];
return $fieldName$questionMark.validate() $orEmptyList;
}
''');
}
}
}
}
Expand Down Expand Up @@ -186,8 +180,8 @@ List<String> $validateName() => _reader.validate<$dartType>('$jsonKey');
String setterName,
String sortOnKey,
) {
if (property.isRequired) {
throw UnimplementedError('Expected an optional property.');
if (!property.type.isNullable) {
throw UnimplementedError('Expected a nullable property.');
}
final fieldName = property.name;
final validateName = property.validateName;
Expand Down Expand Up @@ -301,12 +295,12 @@ List<String> $validateName() => _reader.validateOptionalMap('$jsonKey');
final validateName = property.validateName;
final itemType = dartType.itemType;
final typeName = itemType.toString();
final required = property.isRequired;
final isNullable = property.type.isNullable;

switch (itemType) {
case ClassDartType():
if (required) {
throw UnimplementedError('Expected an optional property.');
if (!isNullable) {
throw UnimplementedError('Expected a nullable property.');
}
buffer.writeln('''
$dartType get $fieldName {
Expand Down Expand Up @@ -351,9 +345,11 @@ List<String> $validateName() {
case SimpleDartType():
switch (itemType.typeName) {
case 'String':
final jsonRead = required ? 'stringList' : 'optionalStringList';
final jsonRead = isNullable ? 'optionalStringList' : 'stringList';
final jsonValidate =
required ? 'validateStringList' : 'validateOptionalStringList';
isNullable
? 'validateOptionalStringList'
: 'validateStringList';
final setter = setOrRemove(dartType, jsonKey);
buffer.writeln('''
$dartType get $fieldName => _reader.$jsonRead('$jsonKey');
Expand All @@ -370,9 +366,9 @@ List<String> $validateName() => _reader.$jsonValidate('$jsonKey');
throw UnimplementedError(itemType.toString());
}
case UriDartType():
final jsonRead = required ? 'pathList' : 'optionalPathList';
final jsonRead = isNullable ? 'optionalPathList' : 'pathList';
final jsonValidate =
required ? 'validatePathList' : 'validateOptionalPathList';
isNullable ? 'validateOptionalPathList' : 'validatePathList';
final setter = setOrRemove(dartType, jsonKey, '.toJson()');
buffer.writeln('''
$dartType get $fieldName => _reader.$jsonRead('$jsonKey');
Expand All @@ -399,9 +395,9 @@ List<String> $validateName() => _reader.$jsonValidate('$jsonKey');
) {
final fieldName = property.name;
final validateName = property.validateName;
final required = property.isRequired;
final jsonRead = required ? r'path$' : 'optionalPath';
final jsonValidate = required ? r'validatePath' : 'validateOptionalPath';
final isNullable = property.type.isNullable;
final jsonRead = isNullable ? 'optionalPath' : r'path$';
final jsonValidate = isNullable ? 'validateOptionalPath' : 'validatePath';
final setter = setOrRemove(dartType, jsonKey, '.toFilePath()');
buffer.writeln('''
$dartType get $fieldName => _reader.$jsonRead('$jsonKey');
Expand Down
35 changes: 35 additions & 0 deletions pkgs/json_syntax_generator/lib/src/model/class_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,15 @@ class NormalClassInfo extends ClassInfo {
bool get isTaggedUnion =>
taggedUnionProperty != null || taggedUnionValue != null;

final List<ConditionallyRequired> extraValidation;

NormalClassInfo({
required super.name,
this.superclass,
required this.properties,
this.taggedUnionProperty,
this.taggedUnionValue,
this.extraValidation = const [],
}) : super() {
superclass?.subclasses.add(this);
if (taggedUnionValue != null) {
Expand All @@ -61,6 +64,9 @@ class NormalClassInfo extends ClassInfo {
final propertiesString = properties
.map((p) => indentLines(p.toString(), level: 2))
.join(',\n');
final extraValidationString = extraValidation
.map((p) => indentLines(p.toString(), level: 2))
.join('\n');
return '''
$runtimeType(
name: $name,
Expand All @@ -71,10 +77,39 @@ $propertiesString
],
taggedUnionProperty: $taggedUnionProperty,
taggedUnionValue: $taggedUnionValue,
extraValidation: [
$extraValidationString
],
)''';
}
}

/// The property [requiredPath] is required if some [conditionPath] has a value
/// in [conditionValues].
///
/// This class is special cased to cover the uses cases seen so far. If
/// different types of conditionals are needed, this class should probably be
/// extended to cover some arbitrary expression.
class ConditionallyRequired {
final List<String> conditionPath;
final List<String> conditionValues;
final List<String> requiredPath;

const ConditionallyRequired({
required this.conditionPath,
required this.conditionValues,
required this.requiredPath,
});

@override
String toString() => '''
ConditionallyRequired(
path: $conditionPath,
values: $conditionValues,
required: $requiredPath,
)''';
}

class EnumClassInfo extends ClassInfo {
final List<EnumValue> enumValues;
final bool isOpen;
Expand Down
3 changes: 0 additions & 3 deletions pkgs/json_syntax_generator/lib/src/model/property_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ class PropertyInfo {
/// meal. See [SchemaAnalyzer.publicSetters].
final bool setterPrivate;

bool get isRequired => !type.isNullable;

PropertyInfo({
required this.name,
required this.jsonKey,
Expand All @@ -53,7 +51,6 @@ PropertyInfo(
type: $type,
isOverride: $isOverride,
setterPrivate: $setterPrivate,
isRequired: $isRequired,
)''';
}

Expand Down
Loading