Improve parsing of hosted dependencies Use new features from json_serializable Allow `host: String` With the map syntax, require a name, but not a url
diff --git a/pkgs/pubspec_parse/CHANGELOG.md b/pkgs/pubspec_parse/CHANGELOG.md index 4fc410a..c0c94c9 100644 --- a/pkgs/pubspec_parse/CHANGELOG.md +++ b/pkgs/pubspec_parse/CHANGELOG.md
@@ -1,6 +1,7 @@ ## 0.1.1 - Fixed name collision with error type in latest `package:json_annotation`. +- Improved parsing of hosted dependencies. ## 0.1.0
diff --git a/pkgs/pubspec_parse/lib/src/dependency.dart b/pkgs/pubspec_parse/lib/src/dependency.dart index dd66d93..1498740 100644 --- a/pkgs/pubspec_parse/lib/src/dependency.dart +++ b/pkgs/pubspec_parse/lib/src/dependency.dart
@@ -4,13 +4,34 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:pub_semver/pub_semver.dart'; +import 'package:yaml/yaml.dart'; part 'dependency.g.dart'; Map<String, Dependency> parseDeps(Map source) => source?.map((k, v) { var key = k as String; - var value = _fromJson(v); + Dependency value; + try { + value = _fromJson(v); + } on CheckedFromJsonException catch (e) { + if (e.map is! YamlMap) { + // This is likely a "synthetic" map created from a String value + // Use `source` to throw this exception with an actual YamlMap and + // extract the associated error information. + + var message = e.message; + var innerError = e.innerError; + // json_annotation should handle FormatException... + // https://github.com/dart-lang/json_serializable/issues/233 + if (innerError is FormatException) { + message = innerError.message; + } + throw new CheckedFromJsonException(source, key, e.className, message); + } + rethrow; + } + if (value == null) { throw new CheckedFromJsonException( source, key, 'Pubspec', 'Not a valid dependency value.'); @@ -141,11 +162,12 @@ String get _info => 'path@$path'; } -@JsonSerializable(createToJson: false) +@JsonSerializable(createToJson: false, disallowUnrecognizedKeys: true) class HostedDependency extends Dependency { @JsonKey(fromJson: _constraintFromString) final VersionConstraint version; + @JsonKey(disallowNullValue: true) final HostedDetails hosted; HostedDependency({VersionConstraint version, this.hosted}) @@ -157,7 +179,8 @@ data = {'version': data}; } - if (data is Map && data.containsKey('version')) { + if (data is Map && + (data.containsKey('version') || data.containsKey('hosted'))) { return _$HostedDependencyFromJson(data); } @@ -168,23 +191,27 @@ String get _info => version.toString(); } -@JsonSerializable(createToJson: false, nullable: false) +@JsonSerializable(createToJson: false, disallowUnrecognizedKeys: true) class HostedDetails { + @JsonKey(required: true, disallowNullValue: true) final String name; - @JsonKey(fromJson: _parseUri) + @JsonKey(fromJson: _parseUri, disallowNullValue: true) final Uri url; - HostedDetails(this.name, this.url) { - if (name == null) { - throw new ArgumentError.value(name, 'name', '"name" cannot be null'); - } - if (url == null) { - throw new ArgumentError.value(url, 'url', '"url" cannot be null'); - } - } + HostedDetails(this.name, this.url); - factory HostedDetails.fromJson(Map json) => _$HostedDetailsFromJson(json); + factory HostedDetails.fromJson(Object data) { + if (data is String) { + data = {'name': data}; + } + + if (data is Map) { + return _$HostedDetailsFromJson(data); + } + + throw new ArgumentError.value(data, 'hosted', 'Must be a Map or String.'); + } } VersionConstraint _constraintFromString(String input) =>
diff --git a/pkgs/pubspec_parse/lib/src/dependency.g.dart b/pkgs/pubspec_parse/lib/src/dependency.g.dart index 620bde7..6133cb4 100644 --- a/pkgs/pubspec_parse/lib/src/dependency.g.dart +++ b/pkgs/pubspec_parse/lib/src/dependency.g.dart
@@ -35,20 +35,28 @@ HostedDependency _$HostedDependencyFromJson(Map json) { return $checkedNew('HostedDependency', json, () { + $checkKeys(json, + allowedKeys: const ['version', 'hosted'], + disallowNullValues: const ['hosted']); var val = new HostedDependency( version: $checkedConvert(json, 'version', (v) => v == null ? null : _constraintFromString(v as String)), hosted: $checkedConvert(json, 'hosted', - (v) => v == null ? null : new HostedDetails.fromJson(v as Map))); + (v) => v == null ? null : new HostedDetails.fromJson(v))); return val; }); } HostedDetails _$HostedDetailsFromJson(Map json) { return $checkedNew('HostedDetails', json, () { + $checkKeys(json, + allowedKeys: const ['name', 'url'], + requiredKeys: const ['name'], + disallowNullValues: const ['name', 'url']); var val = new HostedDetails( $checkedConvert(json, 'name', (v) => v as String), - $checkedConvert(json, 'url', (v) => _parseUri(v as String))); + $checkedConvert( + json, 'url', (v) => v == null ? null : _parseUri(v as String))); return val; }); }
diff --git a/pkgs/pubspec_parse/test/dependency_test.dart b/pkgs/pubspec_parse/test/dependency_test.dart index 527b310..f9fd369 100644 --- a/pkgs/pubspec_parse/test/dependency_test.dart +++ b/pkgs/pubspec_parse/test/dependency_test.dart
@@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:pub_semver/pub_semver.dart'; import 'package:test/test.dart'; import 'test_utils.dart'; @@ -45,28 +46,35 @@ } void _hostedDependency() { - test('HostedDepedency - null', () { + test('null', () { var dep = _dependency<HostedDependency>(null); expect(dep.version.toString(), 'any'); expect(dep.hosted, isNull); expect(dep.toString(), 'HostedDependency: any'); }); - test('HostedDepedency - string', () { + test('string version', () { var dep = _dependency<HostedDependency>('^1.0.0'); expect(dep.version.toString(), '^1.0.0'); expect(dep.hosted, isNull); expect(dep.toString(), 'HostedDependency: ^1.0.0'); }); - test('HostedDepedency - map', () { + test('bad string version', () { + _expectThrows('not a version', r''' +line 4, column 10: Could not parse version "not a version". Unknown text at "not a version". + "dep": "not a version" + ^^^^^^^^^^^^^^^'''); + }); + + test('map w/ just version', () { var dep = _dependency<HostedDependency>({'version': '^1.0.0'}); expect(dep.version.toString(), '^1.0.0'); expect(dep.hosted, isNull); expect(dep.toString(), 'HostedDependency: ^1.0.0'); }); - test('HostedDepedency - map', () { + test('map w/ version and hosted as Map', () { var dep = _dependency<HostedDependency>({ 'version': '^1.0.0', 'hosted': {'name': 'hosted_name', 'url': 'hosted_url'} @@ -76,6 +84,58 @@ expect(dep.hosted.url.toString(), 'hosted_url'); expect(dep.toString(), 'HostedDependency: ^1.0.0'); }); + + test('map w/ bad version value', () { + _expectThrows({ + 'version': 'not a version', + 'hosted': {'name': 'hosted_name', 'url': 'hosted_url'} + }, r''' +line 5, column 15: Unsupported value for `version`. + "version": "not a version", + ^^^^^^^^^^^^^^^'''); + }); + + test('map w/ unsupported keys', () { + _expectThrows({ + 'version': '^1.0.0', + 'hosted': {'name': 'hosted_name', 'url': 'hosted_url'}, + 'not_supported': null + }, r''' +line 4, column 10: Unrecognized keys: [not_supported]; supported keys: [version, hosted] + "dep": { + ^^'''); + }); + + test('map w/ version and hosted as String', () { + var dep = _dependency<HostedDependency>( + {'version': '^1.0.0', 'hosted': 'hosted_name'}); + expect(dep.version.toString(), '^1.0.0'); + expect(dep.hosted.name, 'hosted_name'); + expect(dep.hosted.url, isNull); + expect(dep.toString(), 'HostedDependency: ^1.0.0'); + }); + + test('map w/ hosted as String', () { + var dep = _dependency<HostedDependency>({'hosted': 'hosted_name'}); + expect(dep.version, VersionConstraint.any); + expect(dep.hosted.name, 'hosted_name'); + expect(dep.hosted.url, isNull); + expect(dep.toString(), 'HostedDependency: any'); + }); + + test('map w/ null hosted should error', () { + _expectThrows({'hosted': null}, r''' +line 5, column 14: These keys had `null` values, which is not allowed: [hosted] + "hosted": null + ^^^^^'''); + }); + + test('map w/ null version is fine', () { + var dep = _dependency<HostedDependency>({'version': null}); + expect(dep.version, VersionConstraint.any); + expect(dep.hosted, isNull); + expect(dep.toString(), 'HostedDependency: any'); + }); } void _sdkDependency() {