diff --git a/pkgs/json_syntax_generator/lib/src/generator/helper_library.dart b/pkgs/json_syntax_generator/lib/src/generator/helper_library.dart index 81b7b8687..137278d64 100644 --- a/pkgs/json_syntax_generator/lib/src/generator/helper_library.dart +++ b/pkgs/json_syntax_generator/lib/src/generator/helper_library.dart @@ -6,17 +6,27 @@ /// /// This simplifies the code generator. const helperLib = r''' -extension on Map { +class JsonReader { + /// The JSON Object this reader is reading. + final Map json; + + /// The path traversed by readers of the surrounding JSON. + /// + /// Contains [String] property keys and [int] indices. + /// + /// This is used to give more precise error messages. + final List path; + + JsonReader(this.json, this.path); + T get(String key) { - final value = this[key]; + final value = json[key]; if (value is T) return value; + final pathString = _jsonPathToString([key]); if (value == null) { - throw FormatException('No value was provided for required key: $key'); + throw FormatException("No value was provided for '$pathString'."); } - throw FormatException( - 'Unexpected value \'$value\' for key \'.$key\'. ' - 'Expected a $T.', - ); + throwFormatException(value, T, [key]); } List list(String key) => @@ -29,14 +39,13 @@ extension on Map { }; /// [List.cast] but with [FormatException]s. - static List _castList(List list, String key) { + List _castList(List list, String key) { + var index = 0; for (final value in list) { if (value is! T) { - throw FormatException( - 'Unexpected value \'$list\' (${list.runtimeType}) for key \'.$key\'. ' - 'Expected a ${List}.', - ); + throwFormatException(value, T, [key, index]); } + index++; } return list.cast(); } @@ -60,16 +69,13 @@ extension on Map { }; /// [Map.cast] but with [FormatException]s. - static Map _castMap( + Map _castMap( Map map_, - String key, + String parentKey, ) { - for (final value in map_.values) { + for (final MapEntry(:key, :value) in map_.entries) { if (value is! T) { - throw FormatException( - 'Unexpected value \'$map_\' (${map_.runtimeType}) for key \'.$key\'.' - 'Expected a ${Map}.', - ); + throwFormatException(value, T, [parentKey, key]); } } return map_.cast(); @@ -79,7 +85,7 @@ extension on Map { List stringList(String key) => list(key); - Uri path(String key) => _fileSystemPathToUri(get(key)); + Uri path$(String key) => _fileSystemPathToUri(get(key)); Uri? optionalPath(String key) { final value = get(key); @@ -102,6 +108,23 @@ extension on Map { return Uri.file(path); } + String _jsonPathToString(List pathEnding) => + [...path, ...pathEnding].join('.'); + + Never throwFormatException( + Object? value, + Type expectedType, + List pathExtension, + ) { + final pathString = _jsonPathToString(pathExtension); + throw FormatException( + "Unexpected value '$value' (${value.runtimeType}) for '$pathString'. " + 'Expected a $expectedType.', + ); + } +} + +extension on Map { void setOrRemove(String key, Object? value) { if (value == null) { remove(key); diff --git a/pkgs/json_syntax_generator/lib/src/generator/normal_class_generator.dart b/pkgs/json_syntax_generator/lib/src/generator/normal_class_generator.dart index 423f4bf6d..c33899737 100644 --- a/pkgs/json_syntax_generator/lib/src/generator/normal_class_generator.dart +++ b/pkgs/json_syntax_generator/lib/src/generator/normal_class_generator.dart @@ -72,7 +72,9 @@ class ClassGenerator { if (superclass != null) { buffer.writeln(''' class $className extends $superclassName { - $className.fromJson(super.json) : super.fromJson(); + $className.fromJson(super.json, { + super.path, + }) : super.fromJson(); $className(${wrapBracesIfNotEmpty(constructorParams.join(', '))}) : super(${superParams.join(',')}) @@ -102,9 +104,18 @@ class $className extends $superclassName { class $className { final Map json; - $className.fromJson(this.json); + final List path; - $className(${wrapBracesIfNotEmpty(constructorParams.join(', '))}) : json = {} { + JsonReader get _reader => JsonReader(json, path); + + $className.fromJson(this.json, { + this.path = const [], + }); + + $className(${wrapBracesIfNotEmpty(constructorParams.join(', '))}) + : json = {}, + path = const [] + { ${constructorSetterCalls.join('\n ')} } diff --git a/pkgs/json_syntax_generator/lib/src/generator/property_generator.dart b/pkgs/json_syntax_generator/lib/src/generator/property_generator.dart index 5ebd6b2cd..5e6e30803 100644 --- a/pkgs/json_syntax_generator/lib/src/generator/property_generator.dart +++ b/pkgs/json_syntax_generator/lib/src/generator/property_generator.dart @@ -95,7 +95,7 @@ class PropertyGenerator { buffer.writeln(''' $dartType get $fieldName { - final jsonValue = json.get<$dartStringType>('$jsonKey'); $earlyReturn + final jsonValue = _reader.get<$dartStringType>('$jsonKey'); $earlyReturn return $classType.fromJson(jsonValue); } '''); @@ -115,8 +115,8 @@ set $setterName($dartType value) { final jsonRead = required ? 'map\$' : 'optionalMap'; buffer.writeln(''' $dartType get $fieldName { - final jsonValue = json.$jsonRead('$jsonKey'); $earlyReturn - return $classType.fromJson(jsonValue); + final jsonValue = _reader.$jsonRead('$jsonKey'); $earlyReturn + return $classType.fromJson(jsonValue, path: [...path, '$jsonKey']); } '''); if (!property.isOverride) { @@ -142,7 +142,7 @@ set $setterName($dartType value) { final fieldName = property.name; buffer.writeln(''' -$dartType get $fieldName => json.get<$dartType>('$jsonKey'); +$dartType get $fieldName => _reader.get<$dartType>('$jsonKey'); set $setterName($dartType value) { json.setOrRemove('$jsonKey', value); @@ -169,7 +169,7 @@ set $setterName($dartType value) { case MapDartType(): buffer.writeln(''' $dartType get $fieldName => - json.optionalMap<${dartType.valueType}>('$jsonKey'); + _reader.optionalMap<${dartType.valueType}>('$jsonKey'); set $setterName($dartType value) { json.setOrRemove('$jsonKey', value); @@ -181,17 +181,22 @@ set $setterName($dartType value) { final typeName = itemType.toString(); buffer.writeln(''' $dartType get $fieldName { - final map_ = json.optionalMap('$jsonKey'); - if(map_ == null){ + final jsonValue = _reader.optionalMap('$jsonKey'); + if (jsonValue == null) { return null; } - return { - for (final MapEntry(:key, :value) in map_.entries) - key : [ - for (final item in value as List) - $typeName.fromJson(item as $jsonObjectDartType) - ], - }; + final result = >{}; + for (final MapEntry(:key, :value) in jsonValue.entries) { + var index = 0; + result[key] = [ + for (final item in value as List) + $typeName.fromJson( + item as $jsonObjectDartType, + path: [...path, key, index++], + ), + ]; + } + return result; } set $setterName($dartType value) { @@ -214,7 +219,7 @@ set $setterName($dartType value) { case 'Object': if (valueType.isNullable) { buffer.writeln(''' -$dartType get $fieldName => json.optionalMap('$jsonKey'); +$dartType get $fieldName => _reader.optionalMap('$jsonKey'); set $setterName($dartType value) { json.setOrRemove('$jsonKey', value); @@ -251,8 +256,16 @@ set $setterName($dartType value) { throw UnimplementedError('Expected an optional property.'); } buffer.writeln(''' -$dartType get $fieldName => - json.optionalListParsed('$jsonKey', (e) => $typeName.fromJson(e as Map)); +$dartType get $fieldName { + var index = 0; + return _reader.optionalListParsed( + '$jsonKey', + (e) => $typeName.fromJson( + e as Map, + path: [...path, '$jsonKey', index++], + ), + ); +} set $setterName($dartType value) { if (value == null) { @@ -273,7 +286,7 @@ set $setterName($dartType value) { final jsonRead = required ? 'stringList' : 'optionalStringList'; final setter = setOrRemove(dartType, jsonKey); buffer.writeln(''' -$dartType get $fieldName => json.$jsonRead('$jsonKey'); +$dartType get $fieldName => _reader.$jsonRead('$jsonKey'); set $setterName($dartType value) { $setter @@ -288,7 +301,7 @@ set $setterName($dartType value) { final jsonRead = required ? 'pathList' : 'optionalPathList'; final setter = setOrRemove(dartType, jsonKey, '.toJson()'); buffer.writeln(''' -$dartType get $fieldName => json.$jsonRead('$jsonKey'); +$dartType get $fieldName => _reader.$jsonRead('$jsonKey'); set $setterName($dartType value) { $setter @@ -310,10 +323,10 @@ set $setterName($dartType value) { ) { final fieldName = property.name; final required = property.isRequired; - final jsonRead = required ? 'path' : 'optionalPath'; + final jsonRead = required ? r'path$' : 'optionalPath'; final setter = setOrRemove(dartType, jsonKey, '.toFilePath()'); buffer.writeln(''' -$dartType get $fieldName => json.$jsonRead('$jsonKey'); +$dartType get $fieldName => _reader.$jsonRead('$jsonKey'); set $setterName($dartType value) { $setter diff --git a/pkgs/native_assets_cli/lib/src/code_assets/c_compiler_config.dart b/pkgs/native_assets_cli/lib/src/code_assets/c_compiler_config.dart index d771d89c7..75668b6c4 100644 --- a/pkgs/native_assets_cli/lib/src/code_assets/c_compiler_config.dart +++ b/pkgs/native_assets_cli/lib/src/code_assets/c_compiler_config.dart @@ -37,19 +37,6 @@ final class CCompilerConfig { WindowsCCompilerConfig? windows, }) : _windows = windows; - /// Constructs a [CCompilerConfig] from the given [json]. - /// - /// The json is expected to be valid encoding obtained via - /// [CCompilerConfig.toJson]. - factory CCompilerConfig.fromJson(Map json) => - CCompilerConfigSyntax.fromSyntax(syntax.CCompilerConfig.fromJson(json)); - - /// The json representation of this [CCompilerConfig]. - /// - /// The returned json can be used in [CCompilerConfig.fromJson] to - /// obtain a [CCompilerConfig] again. - Map toJson() => toSyntax().json; - @override bool operator ==(Object other) { if (other is! CCompilerConfig) { diff --git a/pkgs/native_assets_cli/lib/src/code_assets/code_asset.dart b/pkgs/native_assets_cli/lib/src/code_assets/code_asset.dart index c67ed9b4c..4b88ba552 100644 --- a/pkgs/native_assets_cli/lib/src/code_assets/code_asset.dart +++ b/pkgs/native_assets_cli/lib/src/code_assets/code_asset.dart @@ -107,7 +107,10 @@ final class CodeAsset { factory CodeAsset.fromEncoded(EncodedAsset asset) { assert(asset.type == CodeAsset.type); final jsonMap = asset.encoding; - final syntaxNode = syntax.NativeCodeAsset.fromJson(jsonMap); + final syntaxNode = syntax.NativeCodeAsset.fromJson( + jsonMap, + path: asset.jsonPath ?? [], + ); return CodeAsset._( id: syntaxNode.id, os: OSSyntax.fromSyntax(syntaxNode.os), diff --git a/pkgs/native_assets_cli/lib/src/code_assets/config.dart b/pkgs/native_assets_cli/lib/src/code_assets/config.dart index fb479f586..15fff042e 100644 --- a/pkgs/native_assets_cli/lib/src/code_assets/config.dart +++ b/pkgs/native_assets_cli/lib/src/code_assets/config.dart @@ -16,7 +16,7 @@ import 'syntax.g.dart' as syntax; /// to code assets (only available if code assets are supported). extension CodeAssetHookConfig on HookConfig { /// Code asset specific configuration. - CodeConfig get code => CodeConfig._fromJson(json); + CodeConfig get code => CodeConfig._fromJson(json, path); bool get buildCodeAssets => buildAssetTypes.contains(CodeAsset.type); } @@ -41,8 +41,8 @@ extension CodeAssetLinkInput on LinkInputAssets { class CodeConfig { final syntax.CodeConfig _syntax; - CodeConfig._fromJson(Map json) - : _syntax = syntax.Config.fromJson(json).code!; + CodeConfig._fromJson(Map json, List path) + : _syntax = syntax.Config.fromJson(json, path: path).code!; /// The architecture the code code asset should be built for. /// diff --git a/pkgs/native_assets_cli/lib/src/code_assets/syntax.g.dart b/pkgs/native_assets_cli/lib/src/code_assets/syntax.g.dart index 388704285..a719c66e6 100644 --- a/pkgs/native_assets_cli/lib/src/code_assets/syntax.g.dart +++ b/pkgs/native_assets_cli/lib/src/code_assets/syntax.g.dart @@ -11,14 +11,18 @@ import 'dart:io'; class AndroidCodeConfig { final Map json; - AndroidCodeConfig.fromJson(this.json); + final List path; - AndroidCodeConfig({int? targetNdkApi}) : json = {} { + JsonReader get _reader => JsonReader(json, path); + + AndroidCodeConfig.fromJson(this.json, {this.path = const []}); + + AndroidCodeConfig({int? targetNdkApi}) : json = {}, path = const [] { _targetNdkApi = targetNdkApi; json.sortOnKey(); } - int? get targetNdkApi => json.get('target_ndk_api'); + int? get targetNdkApi => _reader.get('target_ndk_api'); set _targetNdkApi(int? value) { json.setOrRemove('target_ndk_api', value); @@ -77,14 +81,18 @@ class Architecture { class Asset { final Map json; - Asset.fromJson(this.json); + final List path; + + JsonReader get _reader => JsonReader(json, path); - Asset({String? type}) : json = {} { + Asset.fromJson(this.json, {this.path = const []}); + + Asset({String? type}) : json = {}, path = const [] { _type = type; json.sortOnKey(); } - String? get type => json.get('type'); + String? get type => _reader.get('type'); set _type(String? value) { json.setOrRemove('type', value); @@ -95,7 +103,7 @@ class Asset { } class NativeCodeAsset extends Asset { - NativeCodeAsset.fromJson(super.json) : super.fromJson(); + NativeCodeAsset.fromJson(super.json, {super.path}) : super.fromJson(); NativeCodeAsset({ Architecture? architecture, @@ -130,7 +138,7 @@ class NativeCodeAsset extends Asset { } Architecture? get architecture { - final jsonValue = json.get('architecture'); + final jsonValue = _reader.get('architecture'); if (jsonValue == null) return null; return Architecture.fromJson(jsonValue); } @@ -139,21 +147,21 @@ class NativeCodeAsset extends Asset { json.setOrRemove('architecture', value?.name); } - Uri? get file => json.optionalPath('file'); + Uri? get file => _reader.optionalPath('file'); set _file(Uri? value) { json.setOrRemove('file', value?.toFilePath()); } - String get id => json.get('id'); + String get id => _reader.get('id'); set _id(String value) { json.setOrRemove('id', value); } LinkMode get linkMode { - final jsonValue = json.map$('link_mode'); - return LinkMode.fromJson(jsonValue); + final jsonValue = _reader.map$('link_mode'); + return LinkMode.fromJson(jsonValue, path: [...path, 'link_mode']); } set _linkMode(LinkMode value) { @@ -161,7 +169,7 @@ class NativeCodeAsset extends Asset { } OS get os { - final jsonValue = json.get('os'); + final jsonValue = _reader.get('os'); return OS.fromJson(jsonValue); } @@ -182,7 +190,11 @@ extension NativeCodeAssetExtension on Asset { class CCompilerConfig { final Map json; - CCompilerConfig.fromJson(this.json); + final List path; + + JsonReader get _reader => JsonReader(json, path); + + CCompilerConfig.fromJson(this.json, {this.path = const []}); CCompilerConfig({ required Uri ar, @@ -191,7 +203,8 @@ class CCompilerConfig { List? envScriptArguments, required Uri ld, Windows? windows, - }) : json = {} { + }) : json = {}, + path = const [] { _ar = ar; _cc = cc; _envScript = envScript; @@ -201,41 +214,41 @@ class CCompilerConfig { json.sortOnKey(); } - Uri get ar => json.path('ar'); + Uri get ar => _reader.path$('ar'); set _ar(Uri value) { json['ar'] = value.toFilePath(); } - Uri get cc => json.path('cc'); + Uri get cc => _reader.path$('cc'); set _cc(Uri value) { json['cc'] = value.toFilePath(); } - Uri? get envScript => json.optionalPath('env_script'); + Uri? get envScript => _reader.optionalPath('env_script'); set _envScript(Uri? value) { json.setOrRemove('env_script', value?.toFilePath()); } List? get envScriptArguments => - json.optionalStringList('env_script_arguments'); + _reader.optionalStringList('env_script_arguments'); set _envScriptArguments(List? value) { json.setOrRemove('env_script_arguments', value); } - Uri get ld => json.path('ld'); + Uri get ld => _reader.path$('ld'); set _ld(Uri value) { json['ld'] = value.toFilePath(); } Windows? get windows { - final jsonValue = json.optionalMap('windows'); + final jsonValue = _reader.optionalMap('windows'); if (jsonValue == null) return null; - return Windows.fromJson(jsonValue); + return Windows.fromJson(jsonValue, path: [...path, 'windows']); } set _windows(Windows? value) { @@ -249,17 +262,26 @@ class CCompilerConfig { class Windows { final Map json; - Windows.fromJson(this.json); + final List path; + + JsonReader get _reader => JsonReader(json, path); + + Windows.fromJson(this.json, {this.path = const []}); - Windows({DeveloperCommandPrompt? developerCommandPrompt}) : json = {} { + Windows({DeveloperCommandPrompt? developerCommandPrompt}) + : json = {}, + path = const [] { _developerCommandPrompt = developerCommandPrompt; json.sortOnKey(); } DeveloperCommandPrompt? get developerCommandPrompt { - final jsonValue = json.optionalMap('developer_command_prompt'); + final jsonValue = _reader.optionalMap('developer_command_prompt'); if (jsonValue == null) return null; - return DeveloperCommandPrompt.fromJson(jsonValue); + return DeveloperCommandPrompt.fromJson( + jsonValue, + path: [...path, 'developer_command_prompt'], + ); } set _developerCommandPrompt(DeveloperCommandPrompt? value) { @@ -273,22 +295,27 @@ class Windows { class DeveloperCommandPrompt { final Map json; - DeveloperCommandPrompt.fromJson(this.json); + final List path; + + JsonReader get _reader => JsonReader(json, path); + + DeveloperCommandPrompt.fromJson(this.json, {this.path = const []}); DeveloperCommandPrompt({required List arguments, required Uri script}) - : json = {} { + : json = {}, + path = const [] { _arguments = arguments; _script = script; json.sortOnKey(); } - List get arguments => json.stringList('arguments'); + List get arguments => _reader.stringList('arguments'); set _arguments(List value) { json['arguments'] = value; } - Uri get script => json.path('script'); + Uri get script => _reader.path$('script'); set _script(Uri value) { json['script'] = value.toFilePath(); @@ -301,7 +328,11 @@ class DeveloperCommandPrompt { class CodeConfig { final Map json; - CodeConfig.fromJson(this.json); + final List path; + + JsonReader get _reader => JsonReader(json, path); + + CodeConfig.fromJson(this.json, {this.path = const []}); CodeConfig({ AndroidCodeConfig? android, @@ -311,7 +342,8 @@ class CodeConfig { MacOSCodeConfig? macOS, required Architecture targetArchitecture, required OS targetOs, - }) : json = {} { + }) : json = {}, + path = const [] { _android = android; _cCompiler = cCompiler; _iOS = iOS; @@ -323,9 +355,9 @@ class CodeConfig { } AndroidCodeConfig? get android { - final jsonValue = json.optionalMap('android'); + final jsonValue = _reader.optionalMap('android'); if (jsonValue == null) return null; - return AndroidCodeConfig.fromJson(jsonValue); + return AndroidCodeConfig.fromJson(jsonValue, path: [...path, 'android']); } set _android(AndroidCodeConfig? value) { @@ -333,9 +365,9 @@ class CodeConfig { } CCompilerConfig? get cCompiler { - final jsonValue = json.optionalMap('c_compiler'); + final jsonValue = _reader.optionalMap('c_compiler'); if (jsonValue == null) return null; - return CCompilerConfig.fromJson(jsonValue); + return CCompilerConfig.fromJson(jsonValue, path: [...path, 'c_compiler']); } set _cCompiler(CCompilerConfig? value) { @@ -343,9 +375,9 @@ class CodeConfig { } IOSCodeConfig? get iOS { - final jsonValue = json.optionalMap('ios'); + final jsonValue = _reader.optionalMap('ios'); if (jsonValue == null) return null; - return IOSCodeConfig.fromJson(jsonValue); + return IOSCodeConfig.fromJson(jsonValue, path: [...path, 'ios']); } set _iOS(IOSCodeConfig? value) { @@ -353,7 +385,7 @@ class CodeConfig { } LinkModePreference get linkModePreference { - final jsonValue = json.get('link_mode_preference'); + final jsonValue = _reader.get('link_mode_preference'); return LinkModePreference.fromJson(jsonValue); } @@ -362,9 +394,9 @@ class CodeConfig { } MacOSCodeConfig? get macOS { - final jsonValue = json.optionalMap('macos'); + final jsonValue = _reader.optionalMap('macos'); if (jsonValue == null) return null; - return MacOSCodeConfig.fromJson(jsonValue); + return MacOSCodeConfig.fromJson(jsonValue, path: [...path, 'macos']); } set _macOS(MacOSCodeConfig? value) { @@ -372,7 +404,7 @@ class CodeConfig { } Architecture get targetArchitecture { - final jsonValue = json.get('target_architecture'); + final jsonValue = _reader.get('target_architecture'); return Architecture.fromJson(jsonValue); } @@ -381,7 +413,7 @@ class CodeConfig { } OS get targetOs { - final jsonValue = json.get('target_os'); + final jsonValue = _reader.get('target_os'); return OS.fromJson(jsonValue); } @@ -396,17 +428,21 @@ class CodeConfig { class Config { final Map json; - Config.fromJson(this.json); + final List path; + + JsonReader get _reader => JsonReader(json, path); + + Config.fromJson(this.json, {this.path = const []}); - Config({CodeConfig? code}) : json = {} { + Config({CodeConfig? code}) : json = {}, path = const [] { this.code = code; json.sortOnKey(); } CodeConfig? get code { - final jsonValue = json.optionalMap('code'); + final jsonValue = _reader.optionalMap('code'); if (jsonValue == null) return null; - return CodeConfig.fromJson(jsonValue); + return CodeConfig.fromJson(jsonValue, path: [...path, 'code']); } set code(CodeConfig? value) { @@ -421,21 +457,27 @@ class Config { class IOSCodeConfig { final Map json; - IOSCodeConfig.fromJson(this.json); + final List path; - IOSCodeConfig({String? targetSdk, int? targetVersion}) : json = {} { + JsonReader get _reader => JsonReader(json, path); + + IOSCodeConfig.fromJson(this.json, {this.path = const []}); + + IOSCodeConfig({String? targetSdk, int? targetVersion}) + : json = {}, + path = const [] { _targetSdk = targetSdk; _targetVersion = targetVersion; json.sortOnKey(); } - String? get targetSdk => json.get('target_sdk'); + String? get targetSdk => _reader.get('target_sdk'); set _targetSdk(String? value) { json.setOrRemove('target_sdk', value); } - int? get targetVersion => json.get('target_version'); + int? get targetVersion => _reader.get('target_version'); set _targetVersion(int? value) { json.setOrRemove('target_version', value); @@ -448,14 +490,18 @@ class IOSCodeConfig { class LinkMode { final Map json; - LinkMode.fromJson(this.json); + final List path; + + JsonReader get _reader => JsonReader(json, path); + + LinkMode.fromJson(this.json, {this.path = const []}); - LinkMode({required String type}) : json = {} { + LinkMode({required String type}) : json = {}, path = const [] { _type = type; json.sortOnKey(); } - String get type => json.get('type'); + String get type => _reader.get('type'); set _type(String value) { json.setOrRemove('type', value); @@ -466,7 +512,8 @@ class LinkMode { } class DynamicLoadingBundleLinkMode extends LinkMode { - DynamicLoadingBundleLinkMode.fromJson(super.json) : super.fromJson(); + DynamicLoadingBundleLinkMode.fromJson(super.json, {super.path}) + : super.fromJson(); DynamicLoadingBundleLinkMode() : super(type: 'dynamic_loading_bundle'); @@ -482,7 +529,8 @@ extension DynamicLoadingBundleLinkModeExtension on LinkMode { } class DynamicLoadingExecutableLinkMode extends LinkMode { - DynamicLoadingExecutableLinkMode.fromJson(super.json) : super.fromJson(); + DynamicLoadingExecutableLinkMode.fromJson(super.json, {super.path}) + : super.fromJson(); DynamicLoadingExecutableLinkMode() : super(type: 'dynamic_loading_executable'); @@ -500,7 +548,8 @@ extension DynamicLoadingExecutableLinkModeExtension on LinkMode { } class DynamicLoadingProcessLinkMode extends LinkMode { - DynamicLoadingProcessLinkMode.fromJson(super.json) : super.fromJson(); + DynamicLoadingProcessLinkMode.fromJson(super.json, {super.path}) + : super.fromJson(); DynamicLoadingProcessLinkMode() : super(type: 'dynamic_loading_process'); @@ -516,7 +565,8 @@ extension DynamicLoadingProcessLinkModeExtension on LinkMode { } class DynamicLoadingSystemLinkMode extends LinkMode { - DynamicLoadingSystemLinkMode.fromJson(super.json) : super.fromJson(); + DynamicLoadingSystemLinkMode.fromJson(super.json, {super.path}) + : super.fromJson(); DynamicLoadingSystemLinkMode({required Uri uri}) : super(type: 'dynamic_loading_system') { @@ -531,7 +581,7 @@ class DynamicLoadingSystemLinkMode extends LinkMode { json.sortOnKey(); } - Uri get uri => json.path('uri'); + Uri get uri => _reader.path$('uri'); set _uri(Uri value) { json['uri'] = value.toFilePath(); @@ -549,7 +599,7 @@ extension DynamicLoadingSystemLinkModeExtension on LinkMode { } class StaticLinkMode extends LinkMode { - StaticLinkMode.fromJson(super.json) : super.fromJson(); + StaticLinkMode.fromJson(super.json, {super.path}) : super.fromJson(); StaticLinkMode() : super(type: 'static'); @@ -606,14 +656,18 @@ class LinkModePreference { class MacOSCodeConfig { final Map json; - MacOSCodeConfig.fromJson(this.json); + final List path; - MacOSCodeConfig({int? targetVersion}) : json = {} { + JsonReader get _reader => JsonReader(json, path); + + MacOSCodeConfig.fromJson(this.json, {this.path = const []}); + + MacOSCodeConfig({int? targetVersion}) : json = {}, path = const [] { _targetVersion = targetVersion; json.sortOnKey(); } - int? get targetVersion => json.get('target_version'); + int? get targetVersion => _reader.get('target_version'); set _targetVersion(int? value) { json.setOrRemove('target_version', value); @@ -660,17 +714,27 @@ class OS { String toString() => name; } -extension on Map { +class JsonReader { + /// The JSON Object this reader is reading. + final Map json; + + /// The path traversed by readers of the surrounding JSON. + /// + /// Contains [String] property keys and [int] indices. + /// + /// This is used to give more precise error messages. + final List path; + + JsonReader(this.json, this.path); + T get(String key) { - final value = this[key]; + final value = json[key]; if (value is T) return value; + final pathString = _jsonPathToString([key]); if (value == null) { - throw FormatException('No value was provided for required key: $key'); + throw FormatException("No value was provided for '$pathString'."); } - throw FormatException( - 'Unexpected value \'$value\' for key \'.$key\'. ' - 'Expected a $T.', - ); + throwFormatException(value, T, [key]); } List list(String key) => @@ -683,14 +747,13 @@ extension on Map { }; /// [List.cast] but with [FormatException]s. - static List _castList(List list, String key) { + List _castList(List list, String key) { + var index = 0; for (final value in list) { if (value is! T) { - throw FormatException( - 'Unexpected value \'$list\' (${list.runtimeType}) for key \'.$key\'. ' - 'Expected a ${List}.', - ); + throwFormatException(value, T, [key, index]); } + index++; } return list.cast(); } @@ -714,16 +777,13 @@ extension on Map { }; /// [Map.cast] but with [FormatException]s. - static Map _castMap( + Map _castMap( Map map_, - String key, + String parentKey, ) { - for (final value in map_.values) { + for (final MapEntry(:key, :value) in map_.entries) { if (value is! T) { - throw FormatException( - 'Unexpected value \'$map_\' (${map_.runtimeType}) for key \'.$key\'.' - 'Expected a ${Map}.', - ); + throwFormatException(value, T, [parentKey, key]); } } return map_.cast(); @@ -733,7 +793,7 @@ extension on Map { List stringList(String key) => list(key); - Uri path(String key) => _fileSystemPathToUri(get(key)); + Uri path$(String key) => _fileSystemPathToUri(get(key)); Uri? optionalPath(String key) { final value = get(key); @@ -756,6 +816,23 @@ extension on Map { return Uri.file(path); } + String _jsonPathToString(List pathEnding) => + [...path, ...pathEnding].join('.'); + + Never throwFormatException( + Object? value, + Type expectedType, + List pathExtension, + ) { + final pathString = _jsonPathToString(pathExtension); + throw FormatException( + "Unexpected value '$value' (${value.runtimeType}) for '$pathString'. " + 'Expected a $expectedType.', + ); + } +} + +extension on Map { void setOrRemove(String key, Object? value) { if (value == null) { remove(key); diff --git a/pkgs/native_assets_cli/lib/src/config.dart b/pkgs/native_assets_cli/lib/src/config.dart index a285ab214..7ca9f3786 100644 --- a/pkgs/native_assets_cli/lib/src/config.dart +++ b/pkgs/native_assets_cli/lib/src/config.dart @@ -245,7 +245,10 @@ final class LinkInputBuilder extends HookInputBuilder { List _parseAssets(List? assets) => assets == null ? [] - : [for (final asset in assets) EncodedAsset.fromJson(asset.json)]; + : [ + for (final asset in assets) + EncodedAsset.fromJson(asset.json, asset.path), + ]; sealed class HookOutput { /// The underlying json configuration of this [HookOutput]. @@ -587,6 +590,8 @@ final latestParsableVersion = Version(1, 7, 0); final class HookConfig { Map get json => _syntax.json; + List get path => _syntax.path; + final syntax.Config _syntax; /// The asset types that should be built by an invocation of a hook. @@ -617,6 +622,9 @@ final class BuildConfig extends HookConfig { bool get linkingEnabled => _syntax.linkingEnabled; BuildConfig._(super.input) - : _syntax = syntax.BuildConfig.fromJson(input._syntax.config.json), + : _syntax = syntax.BuildConfig.fromJson( + input._syntax.config.json, + path: input._syntax.config.path, + ), super._(); } diff --git a/pkgs/native_assets_cli/lib/src/data_assets/syntax.g.dart b/pkgs/native_assets_cli/lib/src/data_assets/syntax.g.dart index 7df687055..d88a736b2 100644 --- a/pkgs/native_assets_cli/lib/src/data_assets/syntax.g.dart +++ b/pkgs/native_assets_cli/lib/src/data_assets/syntax.g.dart @@ -11,14 +11,18 @@ import 'dart:io'; class Asset { final Map json; - Asset.fromJson(this.json); + final List path; - Asset({String? type}) : json = {} { + JsonReader get _reader => JsonReader(json, path); + + Asset.fromJson(this.json, {this.path = const []}); + + Asset({String? type}) : json = {}, path = const [] { _type = type; json.sortOnKey(); } - String? get type => json.get('type'); + String? get type => _reader.get('type'); set _type(String? value) { json.setOrRemove('type', value); @@ -29,7 +33,7 @@ class Asset { } class DataAsset extends Asset { - DataAsset.fromJson(super.json) : super.fromJson(); + DataAsset.fromJson(super.json, {super.path}) : super.fromJson(); DataAsset({required Uri file, required String name, required String package}) : super(type: 'data') { @@ -52,19 +56,19 @@ class DataAsset extends Asset { json.sortOnKey(); } - Uri get file => json.path('file'); + Uri get file => _reader.path$('file'); set _file(Uri value) { json['file'] = value.toFilePath(); } - String get name => json.get('name'); + String get name => _reader.get('name'); set _name(String value) { json.setOrRemove('name', value); } - String get package => json.get('package'); + String get package => _reader.get('package'); set _package(String value) { json.setOrRemove('package', value); @@ -80,17 +84,27 @@ extension DataAssetExtension on Asset { DataAsset get asDataAsset => DataAsset.fromJson(json); } -extension on Map { +class JsonReader { + /// The JSON Object this reader is reading. + final Map json; + + /// The path traversed by readers of the surrounding JSON. + /// + /// Contains [String] property keys and [int] indices. + /// + /// This is used to give more precise error messages. + final List path; + + JsonReader(this.json, this.path); + T get(String key) { - final value = this[key]; + final value = json[key]; if (value is T) return value; + final pathString = _jsonPathToString([key]); if (value == null) { - throw FormatException('No value was provided for required key: $key'); + throw FormatException("No value was provided for '$pathString'."); } - throw FormatException( - 'Unexpected value \'$value\' for key \'.$key\'. ' - 'Expected a $T.', - ); + throwFormatException(value, T, [key]); } List list(String key) => @@ -103,14 +117,13 @@ extension on Map { }; /// [List.cast] but with [FormatException]s. - static List _castList(List list, String key) { + List _castList(List list, String key) { + var index = 0; for (final value in list) { if (value is! T) { - throw FormatException( - 'Unexpected value \'$list\' (${list.runtimeType}) for key \'.$key\'. ' - 'Expected a ${List}.', - ); + throwFormatException(value, T, [key, index]); } + index++; } return list.cast(); } @@ -134,16 +147,13 @@ extension on Map { }; /// [Map.cast] but with [FormatException]s. - static Map _castMap( + Map _castMap( Map map_, - String key, + String parentKey, ) { - for (final value in map_.values) { + for (final MapEntry(:key, :value) in map_.entries) { if (value is! T) { - throw FormatException( - 'Unexpected value \'$map_\' (${map_.runtimeType}) for key \'.$key\'.' - 'Expected a ${Map}.', - ); + throwFormatException(value, T, [parentKey, key]); } } return map_.cast(); @@ -153,7 +163,7 @@ extension on Map { List stringList(String key) => list(key); - Uri path(String key) => _fileSystemPathToUri(get(key)); + Uri path$(String key) => _fileSystemPathToUri(get(key)); Uri? optionalPath(String key) { final value = get(key); @@ -176,6 +186,23 @@ extension on Map { return Uri.file(path); } + String _jsonPathToString(List pathEnding) => + [...path, ...pathEnding].join('.'); + + Never throwFormatException( + Object? value, + Type expectedType, + List pathExtension, + ) { + final pathString = _jsonPathToString(pathExtension); + throw FormatException( + "Unexpected value '$value' (${value.runtimeType}) for '$pathString'. " + 'Expected a $expectedType.', + ); + } +} + +extension on Map { void setOrRemove(String key, Object? value) { if (value == null) { remove(key); diff --git a/pkgs/native_assets_cli/lib/src/encoded_asset.dart b/pkgs/native_assets_cli/lib/src/encoded_asset.dart index 959f3a6f0..39cf5d165 100644 --- a/pkgs/native_assets_cli/lib/src/encoded_asset.dart +++ b/pkgs/native_assets_cli/lib/src/encoded_asset.dart @@ -15,15 +15,23 @@ final class EncodedAsset { /// The json encoding of the asset. final Map encoding; - EncodedAsset(this.type, this.encoding); + /// The path of this object in a larger JSON. + /// + /// If provided, used for more precise error messages. + final List? jsonPath; + + EncodedAsset(this.type, this.encoding, {this.jsonPath}); /// Decode an [EncodedAsset] from json. - factory EncodedAsset.fromJson(Map json) { + factory EncodedAsset.fromJson( + Map json, [ + List? path, + ]) { final syntax_ = syntax.Asset.fromJson(json); return EncodedAsset(syntax_.type, { for (final key in json.keys) if (key != _typeKey) key: json[key], - }); + }, jsonPath: path); } /// Encode this [EncodedAsset] tojson. diff --git a/pkgs/native_assets_cli/lib/src/hook/syntax.g.dart b/pkgs/native_assets_cli/lib/src/hook/syntax.g.dart index f57a03cd4..bb2b4492a 100644 --- a/pkgs/native_assets_cli/lib/src/hook/syntax.g.dart +++ b/pkgs/native_assets_cli/lib/src/hook/syntax.g.dart @@ -11,14 +11,18 @@ import 'dart:io'; class Asset { final Map json; - Asset.fromJson(this.json); + final List path; - Asset({required String type}) : json = {} { + JsonReader get _reader => JsonReader(json, path); + + Asset.fromJson(this.json, {this.path = const []}); + + Asset({required String type}) : json = {}, path = const [] { _type = type; json.sortOnKey(); } - String get type => json.get('type'); + String get type => _reader.get('type'); set _type(String value) { json.setOrRemove('type', value); @@ -29,7 +33,7 @@ class Asset { } class BuildConfig extends Config { - BuildConfig.fromJson(super.json) : super.fromJson(); + BuildConfig.fromJson(super.json, {super.path}) : super.fromJson(); BuildConfig({required super.buildAssetTypes, required bool linkingEnabled}) : super() { @@ -44,7 +48,7 @@ class BuildConfig extends Config { json.sortOnKey(); } - bool get linkingEnabled => json.get('linking_enabled'); + bool get linkingEnabled => _reader.get('linking_enabled'); set _linkingEnabled(bool value) { json.setOrRemove('linking_enabled', value); @@ -55,7 +59,7 @@ class BuildConfig extends Config { } class BuildInput extends HookInput { - BuildInput.fromJson(super.json) : super.fromJson(); + BuildInput.fromJson(super.json, {super.path}) : super.fromJson(); BuildInput({ required super.config, @@ -80,12 +84,12 @@ class BuildInput extends HookInput { @override BuildConfig get config { - final jsonValue = json.map$('config'); - return BuildConfig.fromJson(jsonValue); + final jsonValue = _reader.map$('config'); + return BuildConfig.fromJson(jsonValue, path: [...path, 'config']); } Map>? get dependencyMetadata => - json.optionalMap>('dependency_metadata'); + _reader.optionalMap>('dependency_metadata'); set _dependencyMetadata(Map>? value) { json.setOrRemove('dependency_metadata', value); @@ -96,7 +100,7 @@ class BuildInput extends HookInput { } class BuildOutput extends HookOutput { - BuildOutput.fromJson(super.json) : super.fromJson(); + BuildOutput.fromJson(super.json, {super.path}) : super.fromJson(); BuildOutput({ super.assets, @@ -123,17 +127,22 @@ class BuildOutput extends HookOutput { } Map>? get assetsForLinking { - final map_ = json.optionalMap('assetsForLinking'); - if (map_ == null) { + final jsonValue = _reader.optionalMap('assetsForLinking'); + if (jsonValue == null) { return null; } - return { - for (final MapEntry(:key, :value) in map_.entries) - key: [ - for (final item in value as List) - Asset.fromJson(item as Map), - ], - }; + final result = >{}; + for (final MapEntry(:key, :value) in jsonValue.entries) { + var index = 0; + result[key] = [ + for (final item in value as List) + Asset.fromJson( + item as Map, + path: [...path, key, index++], + ), + ]; + } + return result; } set assetsForLinking(Map>? value) { @@ -148,7 +157,7 @@ class BuildOutput extends HookOutput { json.sortOnKey(); } - Map? get metadata => json.optionalMap('metadata'); + Map? get metadata => _reader.optionalMap('metadata'); set metadata(Map? value) { json.setOrRemove('metadata', value); @@ -162,14 +171,18 @@ class BuildOutput extends HookOutput { class Config { final Map json; - Config.fromJson(this.json); + final List path; + + JsonReader get _reader => JsonReader(json, path); - Config({required List buildAssetTypes}) : json = {} { + Config.fromJson(this.json, {this.path = const []}); + + Config({required List buildAssetTypes}) : json = {}, path = const [] { this.buildAssetTypes = buildAssetTypes; json.sortOnKey(); } - List get buildAssetTypes => json.stringList('build_asset_types'); + List get buildAssetTypes => _reader.stringList('build_asset_types'); set buildAssetTypes(List value) { json['build_asset_types'] = value; @@ -183,7 +196,11 @@ class Config { class HookInput { final Map json; - HookInput.fromJson(this.json); + final List path; + + JsonReader get _reader => JsonReader(json, path); + + HookInput.fromJson(this.json, {this.path = const []}); HookInput({ required Config config, @@ -193,7 +210,8 @@ class HookInput { required String packageName, required Uri packageRoot, required String version, - }) : json = {} { + }) : json = {}, + path = const [] { this.config = config; this.outDir = outDir; this.outDirShared = outDirShared; @@ -205,8 +223,8 @@ class HookInput { } Config get config { - final jsonValue = json.map$('config'); - return Config.fromJson(jsonValue); + final jsonValue = _reader.map$('config'); + return Config.fromJson(jsonValue, path: [...path, 'config']); } set config(Config value) { @@ -214,42 +232,42 @@ class HookInput { json.sortOnKey(); } - Uri get outDir => json.path('out_dir'); + Uri get outDir => _reader.path$('out_dir'); set outDir(Uri value) { json['out_dir'] = value.toFilePath(); json.sortOnKey(); } - Uri get outDirShared => json.path('out_dir_shared'); + Uri get outDirShared => _reader.path$('out_dir_shared'); set outDirShared(Uri value) { json['out_dir_shared'] = value.toFilePath(); json.sortOnKey(); } - Uri? get outFile => json.optionalPath('out_file'); + Uri? get outFile => _reader.optionalPath('out_file'); set outFile(Uri? value) { json.setOrRemove('out_file', value?.toFilePath()); json.sortOnKey(); } - String get packageName => json.get('package_name'); + String get packageName => _reader.get('package_name'); set packageName(String value) { json.setOrRemove('package_name', value); json.sortOnKey(); } - Uri get packageRoot => json.path('package_root'); + Uri get packageRoot => _reader.path$('package_root'); set packageRoot(Uri value) { json['package_root'] = value.toFilePath(); json.sortOnKey(); } - String get version => json.get('version'); + String get version => _reader.get('version'); set version(String value) { json.setOrRemove('version', value); @@ -263,14 +281,19 @@ class HookInput { class HookOutput { final Map json; - HookOutput.fromJson(this.json); + final List path; + + JsonReader get _reader => JsonReader(json, path); + + HookOutput.fromJson(this.json, {this.path = const []}); HookOutput({ List? assets, List? dependencies, required String timestamp, required String version, - }) : json = {} { + }) : json = {}, + path = const [] { this.assets = assets; this.dependencies = dependencies; this.timestamp = timestamp; @@ -278,10 +301,16 @@ class HookOutput { json.sortOnKey(); } - List? get assets => json.optionalListParsed( - 'assets', - (e) => Asset.fromJson(e as Map), - ); + List? get assets { + var index = 0; + return _reader.optionalListParsed( + 'assets', + (e) => Asset.fromJson( + e as Map, + path: [...path, 'assets', index++], + ), + ); + } set assets(List? value) { if (value == null) { @@ -292,21 +321,21 @@ class HookOutput { json.sortOnKey(); } - List? get dependencies => json.optionalPathList('dependencies'); + List? get dependencies => _reader.optionalPathList('dependencies'); set dependencies(List? value) { json.setOrRemove('dependencies', value?.toJson()); json.sortOnKey(); } - String get timestamp => json.get('timestamp'); + String get timestamp => _reader.get('timestamp'); set timestamp(String value) { json.setOrRemove('timestamp', value); json.sortOnKey(); } - String get version => json.get('version'); + String get version => _reader.get('version'); set version(String value) { json.setOrRemove('version', value); @@ -318,7 +347,7 @@ class HookOutput { } class LinkInput extends HookInput { - LinkInput.fromJson(super.json) : super.fromJson(); + LinkInput.fromJson(super.json, {super.path}) : super.fromJson(); LinkInput({ List? assets, @@ -344,10 +373,16 @@ class LinkInput extends HookInput { json.sortOnKey(); } - List? get assets => json.optionalListParsed( - 'assets', - (e) => Asset.fromJson(e as Map), - ); + List? get assets { + var index = 0; + return _reader.optionalListParsed( + 'assets', + (e) => Asset.fromJson( + e as Map, + path: [...path, 'assets', index++], + ), + ); + } set _assets(List? value) { if (value == null) { @@ -357,7 +392,7 @@ class LinkInput extends HookInput { } } - Uri? get resourceIdentifiers => json.optionalPath('resource_identifiers'); + Uri? get resourceIdentifiers => _reader.optionalPath('resource_identifiers'); set _resourceIdentifiers(Uri? value) { json.setOrRemove('resource_identifiers', value?.toFilePath()); @@ -368,7 +403,7 @@ class LinkInput extends HookInput { } class LinkOutput extends HookOutput { - LinkOutput.fromJson(super.json) : super.fromJson(); + LinkOutput.fromJson(super.json, {super.path}) : super.fromJson(); LinkOutput({ super.assets, @@ -381,17 +416,27 @@ class LinkOutput extends HookOutput { String toString() => 'LinkOutput($json)'; } -extension on Map { +class JsonReader { + /// The JSON Object this reader is reading. + final Map json; + + /// The path traversed by readers of the surrounding JSON. + /// + /// Contains [String] property keys and [int] indices. + /// + /// This is used to give more precise error messages. + final List path; + + JsonReader(this.json, this.path); + T get(String key) { - final value = this[key]; + final value = json[key]; if (value is T) return value; + final pathString = _jsonPathToString([key]); if (value == null) { - throw FormatException('No value was provided for required key: $key'); + throw FormatException("No value was provided for '$pathString'."); } - throw FormatException( - 'Unexpected value \'$value\' for key \'.$key\'. ' - 'Expected a $T.', - ); + throwFormatException(value, T, [key]); } List list(String key) => @@ -404,14 +449,13 @@ extension on Map { }; /// [List.cast] but with [FormatException]s. - static List _castList(List list, String key) { + List _castList(List list, String key) { + var index = 0; for (final value in list) { if (value is! T) { - throw FormatException( - 'Unexpected value \'$list\' (${list.runtimeType}) for key \'.$key\'. ' - 'Expected a ${List}.', - ); + throwFormatException(value, T, [key, index]); } + index++; } return list.cast(); } @@ -435,16 +479,13 @@ extension on Map { }; /// [Map.cast] but with [FormatException]s. - static Map _castMap( + Map _castMap( Map map_, - String key, + String parentKey, ) { - for (final value in map_.values) { + for (final MapEntry(:key, :value) in map_.entries) { if (value is! T) { - throw FormatException( - 'Unexpected value \'$map_\' (${map_.runtimeType}) for key \'.$key\'.' - 'Expected a ${Map}.', - ); + throwFormatException(value, T, [parentKey, key]); } } return map_.cast(); @@ -454,7 +495,7 @@ extension on Map { List stringList(String key) => list(key); - Uri path(String key) => _fileSystemPathToUri(get(key)); + Uri path$(String key) => _fileSystemPathToUri(get(key)); Uri? optionalPath(String key) { final value = get(key); @@ -477,6 +518,23 @@ extension on Map { return Uri.file(path); } + String _jsonPathToString(List pathEnding) => + [...path, ...pathEnding].join('.'); + + Never throwFormatException( + Object? value, + Type expectedType, + List pathExtension, + ) { + final pathString = _jsonPathToString(pathExtension); + throw FormatException( + "Unexpected value '$value' (${value.runtimeType}) for '$pathString'. " + 'Expected a $expectedType.', + ); + } +} + +extension on Map { void setOrRemove(String key, Object? value) { if (value == null) { remove(key); diff --git a/pkgs/native_assets_cli/test/build_config_test.dart b/pkgs/native_assets_cli/test/build_input_test.dart similarity index 66% rename from pkgs/native_assets_cli/test/build_config_test.dart rename to pkgs/native_assets_cli/test/build_input_test.dart index 47e421f42..8e94fe3c2 100644 --- a/pkgs/native_assets_cli/test/build_config_test.dart +++ b/pkgs/native_assets_cli/test/build_input_test.dart @@ -11,6 +11,8 @@ import 'package:native_assets_cli/native_assets_cli_builder.dart'; import 'package:native_assets_cli/src/config.dart' show latestVersion; import 'package:test/test.dart'; +import 'helpers.dart'; + void main() async { late Uri outFile; late Uri outDirUri; @@ -18,6 +20,7 @@ void main() async { late String packageName; late Uri packageRootUri; late Map metadata; + late Map inputJson; setUp(() async { final tempUri = Directory.systemTemp.uri; @@ -33,24 +36,8 @@ void main() async { }), 'foo': const Metadata({'key': 321}), }; - }); - test('BuildInputBuilder->JSON->BuildInput', () { - final inputBuilder = - BuildInputBuilder() - ..setupShared( - packageName: packageName, - packageRoot: packageRootUri, - outputFile: outFile, - outputDirectory: outDirUri, - outputDirectoryShared: outputDirectoryShared, - ) - ..config.setupShared(buildAssetTypes: ['my-asset-type']) - ..config.setupBuild(linkingEnabled: false) - ..setupBuildInput(metadata: metadata); - final input = BuildInput(inputBuilder.json); - - final expectedInputJson = { + inputJson = { 'config': { 'build_asset_types': ['my-asset-type'], 'linking_enabled': false, @@ -69,9 +56,25 @@ void main() async { 'package_root': packageRootUri.toFilePath(), 'version': latestVersion.toString(), }; + }); - expect(input.json, expectedInputJson); - expect(json.decode(input.toString()), expectedInputJson); + test('BuildInputBuilder->JSON->BuildInput', () { + final inputBuilder = + BuildInputBuilder() + ..setupShared( + packageName: packageName, + packageRoot: packageRootUri, + outputFile: outFile, + outputDirectory: outDirUri, + outputDirectoryShared: outputDirectoryShared, + ) + ..config.setupShared(buildAssetTypes: ['my-asset-type']) + ..config.setupBuild(linkingEnabled: false) + ..setupBuildInput(metadata: metadata); + final input = BuildInput(inputBuilder.json); + + expect(input.json, inputJson); + expect(json.decode(input.toString()), inputJson); expect(input.outputDirectory, outDirUri); expect(input.outputDirectoryShared, outputDirectoryShared); @@ -87,21 +90,8 @@ void main() async { group('BuildInput format issues', () { for (final version in ['9001.0.0', '0.0.1']) { test('BuildInput version $version', () { - final outDir = outDirUri; - final input = { - 'config': { - 'build_asset_types': ['my-asset-type'], - 'linking_enabled': false, - 'target_os': 'linux', - 'link_mode_preference': 'prefer-static', - }, - 'out_dir': outDir.toFilePath(), - 'out_dir_shared': outputDirectoryShared.toFilePath(), - 'out_file': outFile.toFilePath(), - 'package_root': packageRootUri.toFilePath(), - 'version': version, - 'package_name': packageName, - }; + final input = inputJson; + input['version'] = version; expect( () => BuildInput(input), throwsA( @@ -116,59 +106,55 @@ void main() async { }); } - test('BuildInput FormatExceptions', () { + test('BuildInput FormatException out_dir', () { + final input = inputJson; + input.remove('out_dir'); expect( - () => BuildInput({}), + () => BuildInput(input), throwsA( predicate( (e) => e is FormatException && - e.message.contains('No value was provided for required key: '), + e.message.contains("No value was provided for 'out_dir'."), ), ), ); + }); + + test('BuildInput FormatException dependency_metadata', () { + final input = inputJson; + input['dependency_metadata'] = { + 'bar': {'key': 'value'}, + 'foo': [], + }; expect( - () => BuildInput({ - 'version': latestVersion.toString(), - 'package_name': packageName, - 'package_root': packageRootUri.toFilePath(), - 'target_os': 'android', - }), + () => BuildInput(input), throwsA( predicate( (e) => e is FormatException && - e.message.contains( - 'No value was provided for required key: out_dir', - ), + e.message.contains('Unexpected value') && + e.message.contains('dependency_metadata.foo') && + e.message.contains('Expected a Map'), ), ), ); + }); + + test('BuildInput FormatException config.build_asset_types', () { + final input = inputJson; + traverseJson>(input, [ + 'config', + ]).remove('build_asset_types'); expect( - () => BuildInput({ - 'config': { - 'build_asset_types': ['my-asset-type'], - 'linking_enabled': false, - }, - 'version': latestVersion.toString(), - 'out_dir': outDirUri.toFilePath(), - 'out_dir_shared': outputDirectoryShared.toFilePath(), - 'out_file': outFile.toFilePath(), - 'package_name': packageName, - 'package_root': packageRootUri.toFilePath(), - 'target_os': 'android', - 'dependency_metadata': { - 'bar': {'key': 'value'}, - 'foo': [], - }, - }), + () => BuildInput(input).config.buildAssetTypes, throwsA( predicate( (e) => e is FormatException && - e.message.contains('Unexpected value') && e.message.contains( - 'Expected a Map>', + 'No value was provided for ' + "'config.build_asset_types'.", ), ), ), diff --git a/pkgs/native_assets_cli/test/code_assets/config_test.dart b/pkgs/native_assets_cli/test/code_assets/config_test.dart index e47803d7c..c94ff007c 100644 --- a/pkgs/native_assets_cli/test/code_assets/config_test.dart +++ b/pkgs/native_assets_cli/test/code_assets/config_test.dart @@ -5,9 +5,10 @@ import 'dart:io'; import 'package:native_assets_cli/code_assets_builder.dart'; -import 'package:native_assets_cli/src/config.dart' show latestVersion; import 'package:test/test.dart'; +import '../helpers.dart'; + void main() async { late Uri outDirUri; late Uri outputDirectoryShared; @@ -49,7 +50,7 @@ void main() async { // check the nested key. Map inputJson({ String hookType = 'build', - bool includeDeprecated = true, + bool includeDeprecated = false, OS targetOS = OS.android, }) => { if (hookType == 'link') @@ -148,15 +149,13 @@ void main() async { ), ); final input = BuildInput(inputBuilder.json); - expect(input.json, inputJson()); + expect(input.json, inputJson(includeDeprecated: true)); expectCorrectCodeConfig(input.config.code); }); test('BuildInput from json without deprecated keys', () { for (final targetOS in [OS.android, OS.iOS, OS.macOS]) { - final input = BuildInput( - inputJson(includeDeprecated: false, targetOS: targetOS), - ); + final input = BuildInput(inputJson(targetOS: targetOS)); expect(input.packageName, packageName); expect(input.packageRoot, packageRootUri); expect(input.outputDirectory, outDirUri); @@ -197,7 +196,7 @@ void main() async { ), ); final input = LinkInput(inputBuilder.json); - expect(input.json, inputJson(hookType: 'link')); + expect(input.json, inputJson(hookType: 'link', includeDeprecated: true)); expectCorrectCodeConfig(input.config.code); expect(input.assets.encodedAssets, assets); }); @@ -221,23 +220,12 @@ void main() async { }); test('BuildInput.config.code: invalid architecture', () { - final input = { - 'config': { - 'code': { - 'link_mode_preference': 'prefer-static', - 'android': {'target_ndk_api': 30}, - 'target_architecture': 'invalid_architecture', - 'target_os': 'android', - }, - 'linking_enabled': false, - }, - 'out_dir': outDirUri.toFilePath(), - 'out_dir_shared': outputDirectoryShared.toFilePath(), - 'out_file': outFile.toFilePath(), - 'package_name': packageName, - 'package_root': packageRootUri.toFilePath(), - 'version': latestVersion.toString(), - }; + final input = inputJson(); + traverseJson>(input, [ + 'config', + 'code', + ])['target_architecture'] = + 'invalid_architecture'; expect( () => BuildInput(input).config.code.targetArchitecture, throwsFormatException, @@ -245,22 +233,67 @@ void main() async { }); test('LinkInput.config.code: invalid os', () { - final input = { - 'config': { - 'code': { - 'link_mode_preference': 'prefer-static', - 'android': {'target_ndk_api': 30}, - 'target_architecture': 'x64', - 'target_os': 'invalid_os', - }, - }, - 'out_dir': outDirUri.toFilePath(), - 'out_dir_shared': outputDirectoryShared.toFilePath(), - 'out_file': outFile.toFilePath(), - 'package_name': packageName, - 'package_root': packageRootUri.toFilePath(), - 'version': latestVersion.toString(), - }; + final input = inputJson(hookType: 'link'); + traverseJson>(input, ['config', 'code'])['target_os'] = + 'invalid_os'; expect(() => LinkInput(input).config.code.targetOS, throwsFormatException); }); + + test('LinkInput.config.code.target_os invalid type', () { + final input = inputJson(hookType: 'link'); + traverseJson>(input, ['config', 'code'])['target_os'] = + 123; + expect( + () => LinkInput(input).config.code.targetOS, + throwsA( + predicate( + (e) => + e is FormatException && + e.message.contains( + "Unexpected value '123' (int) for 'config.code.target_os'. " + 'Expected a String.', + ), + ), + ), + ); + }); + + test('LinkInput.config.code.link_mode_preference missing', () { + final input = inputJson(hookType: 'link'); + traverseJson>(input, [ + 'config', + 'code', + ]).remove('link_mode_preference'); + expect( + () => LinkInput(input).config.code.linkModePreference, + throwsA( + predicate( + (e) => + e is FormatException && + e.message.contains( + "No value was provided for 'config.code.link_mode_preference'.", + ), + ), + ), + ); + }); + test('LinkInput.assets.0.link_mode missing', () { + final input = inputJson(hookType: 'link'); + traverseJson>(input, [ + 'assets', + 0, + ]).remove('link_mode'); + expect( + () => LinkInput(input).assets.code.first.linkMode, + throwsA( + predicate( + (e) => + e is FormatException && + e.message.contains( + "No value was provided for 'assets.0.link_mode'.", + ), + ), + ), + ); + }); } diff --git a/pkgs/native_assets_cli/test/code_assets/validation_test.dart b/pkgs/native_assets_cli/test/code_assets/validation_test.dart index 4873fead9..415cc5b2b 100644 --- a/pkgs/native_assets_cli/test/code_assets/validation_test.dart +++ b/pkgs/native_assets_cli/test/code_assets/validation_test.dart @@ -8,6 +8,8 @@ import 'package:native_assets_cli/code_assets_builder.dart'; import 'package:native_assets_cli/src/code_assets/validation.dart'; import 'package:test/test.dart'; +import '../helpers.dart'; + void main() { late Uri tempUri; late Uri outDirUri; @@ -326,20 +328,3 @@ void main() { }); }); } - -T traverseJson(Object json, List path) { - while (path.isNotEmpty) { - final key = path.removeAt(0); - switch (key) { - case final int i: - json = (json as List)[i] as Object; - break; - case final String s: - json = (json as Map)[s] as Object; - break; - default: - throw UnsupportedError(key.toString()); - } - } - return json as T; -} diff --git a/pkgs/native_assets_cli/test/helpers.dart b/pkgs/native_assets_cli/test/helpers.dart index 2a3815d78..bcc86c776 100644 --- a/pkgs/native_assets_cli/test/helpers.dart +++ b/pkgs/native_assets_cli/test/helpers.dart @@ -202,3 +202,20 @@ Logger _createTestLogger({ final dartExecutable = File(Platform.resolvedExecutable).uri; int defaultMacOSVersion = 13; + +T traverseJson(Object json, List path) { + while (path.isNotEmpty) { + final key = path.removeAt(0); + switch (key) { + case final int i: + json = (json as List)[i] as Object; + break; + case final String s: + json = (json as Map)[s] as Object; + break; + default: + throw UnsupportedError(key.toString()); + } + } + return json as T; +} diff --git a/pkgs/native_assets_cli/test/link_config_test.dart b/pkgs/native_assets_cli/test/link_input_test.dart similarity index 68% rename from pkgs/native_assets_cli/test/link_config_test.dart rename to pkgs/native_assets_cli/test/link_input_test.dart index 32785e9a6..e3f720488 100644 --- a/pkgs/native_assets_cli/test/link_config_test.dart +++ b/pkgs/native_assets_cli/test/link_input_test.dart @@ -16,6 +16,7 @@ void main() async { late String packageName; late Uri packageRootUri; late List assets; + late Map inputJson; setUp(() async { final tempUri = Directory.systemTemp.uri; @@ -28,6 +29,19 @@ void main() async { for (int i = 0; i < 3; i++) EncodedAsset('my-asset-type', {'a-$i': 'v-$i'}), ]; + + inputJson = { + 'assets': [for (final asset in assets) asset.toJson()], + 'config': { + 'build_asset_types': ['asset-type-1', 'asset-type-2'], + }, + 'out_dir_shared': outputDirectoryShared.toFilePath(), + 'out_dir': outDirUri.toFilePath(), + 'out_file': outFile.toFilePath(), + 'package_name': packageName, + 'package_root': packageRootUri.toFilePath(), + 'version': latestVersion.toString(), + }; }); test('LinkInputBuilder->JSON->LinkInput', () { @@ -46,20 +60,8 @@ void main() async { ..setupLink(assets: assets, recordedUsesFile: null); final input = LinkInput(inputBuilder.json); - final expectedInputJson = { - 'assets': [for (final asset in assets) asset.toJson()], - 'config': { - 'build_asset_types': ['asset-type-1', 'asset-type-2'], - }, - 'out_dir_shared': outputDirectoryShared.toFilePath(), - 'out_dir': outDirUri.toFilePath(), - 'out_file': outFile.toFilePath(), - 'package_name': packageName, - 'package_root': packageRootUri.toFilePath(), - 'version': latestVersion.toString(), - }; - expect(input.json, expectedInputJson); - expect(json.decode(input.toString()), expectedInputJson); + expect(input.json, inputJson); + expect(json.decode(input.toString()), inputJson); expect(input.outputDirectory, outDirUri); expect(input.outputDirectoryShared, outputDirectoryShared); @@ -73,15 +75,8 @@ void main() async { group('LinkInput FormatExceptions', () { for (final version in ['9001.0.0', '0.0.1']) { test('LinkInput version $version', () { - final outDir = outDirUri; - final input = { - 'out_dir': outDir.toFilePath(), - 'out_dir_shared': outputDirectoryShared.toFilePath(), - 'out_file': outFile.toFilePath(), - 'package_root': packageRootUri.toFilePath(), - 'version': version, - 'package_name': packageName, - }; + final input = inputJson; + input['version'] = version; expect( () => LinkInput(input), throwsA( @@ -95,50 +90,33 @@ void main() async { ); }); } - test('LinkInput FormatExceptions', () { - expect( - () => LinkInput({}), - throwsA( - predicate( - (e) => - e is FormatException && - e.message.contains('No value was provided for required key: '), - ), - ), - ); + + test('LinkInput FormatException out_dir', () { + final input = inputJson; + input.remove('out_dir'); expect( - () => LinkInput({ - 'version': latestVersion.toString(), - 'package_name': packageName, - 'package_root': packageRootUri.toFilePath(), - 'assets': [], - }), + () => LinkInput(input), throwsA( predicate( (e) => e is FormatException && - e.message.contains( - 'No value was provided for required key: out_dir', - ), + e.message.contains("No value was provided for 'out_dir'."), ), ), ); + }); + + test('LinkInput FormatExceptions', () { + final input = inputJson; + input['assets'] = 'astring'; expect( - () => LinkInput({ - 'version': latestVersion.toString(), - 'out_dir': outDirUri.toFilePath(), - 'out_dir_shared': outputDirectoryShared.toFilePath(), - 'out_file': outFile.toFilePath(), - 'package_name': packageName, - 'package_root': packageRootUri.toFilePath(), - 'assets': 'astring', - }), + () => LinkInput(input), throwsA( predicate( (e) => e is FormatException && e.message.contains( - "Unexpected value 'astring' for key '.assets'. " + "Unexpected value 'astring' (String) for 'assets'. " 'Expected a List?.', ), ),