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() {