Add support for publish_to field

Fixes https://github.com/dart-lang/pubspec_parse/issues/21
diff --git a/pkgs/pubspec_parse/CHANGELOG.md b/pkgs/pubspec_parse/CHANGELOG.md
index 26de02e..add9737 100644
--- a/pkgs/pubspec_parse/CHANGELOG.md
+++ b/pkgs/pubspec_parse/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.1.3
+
+- Add support for `publish_to` field.
+
 ## 0.1.2+3
 
 - Support the latest version of `package:json_annotation`.
diff --git a/pkgs/pubspec_parse/lib/src/pubspec.dart b/pkgs/pubspec_parse/lib/src/pubspec.dart
index dee92e0..27ebc64 100644
--- a/pkgs/pubspec_parse/lib/src/pubspec.dart
+++ b/pkgs/pubspec_parse/lib/src/pubspec.dart
@@ -14,7 +14,6 @@
 @JsonSerializable()
 class Pubspec {
   // TODO: executables
-  // TODO: publish_to
 
   final String name;
 
@@ -24,6 +23,15 @@
   final String description;
   final String homepage;
 
+  /// Specifies where to publish this package.
+  ///
+  /// Accepted values: `null`, `'none'` or an `http` or `https` URL.
+  ///
+  /// If not specified, the pub client defaults to `https://pub.dartlang.org`.
+  ///
+  /// [More information](https://www.dartlang.org/tools/pub/pubspec#publish_to).
+  final String publishTo;
+
   /// If there is exactly 1 value in [authors], returns it.
   ///
   /// If there are 0 or more than 1, returns `null`.
@@ -56,6 +64,7 @@
   Pubspec(
     this.name, {
     this.version,
+    this.publishTo,
     String author,
     List<String> authors,
     Map<String, VersionConstraint> environment,
@@ -73,6 +82,17 @@
     if (name == null || name.isEmpty) {
       throw ArgumentError.value(name, 'name', '"name" cannot be empty.');
     }
+
+    if (publishTo != null && publishTo != 'none') {
+      try {
+        final targetUri = Uri.parse(publishTo);
+        if (!(targetUri.isScheme('http') || targetUri.isScheme('https'))) {
+          throw const FormatException('must be an http or https URL.');
+        }
+      } on FormatException catch (e) {
+        throw ArgumentError.value(publishTo, 'publishTo', e.message);
+      }
+    }
   }
 
   factory Pubspec.fromJson(Map json) => _$PubspecFromJson(json);
diff --git a/pkgs/pubspec_parse/lib/src/pubspec.g.dart b/pkgs/pubspec_parse/lib/src/pubspec.g.dart
index 08ad546..bf878c2 100644
--- a/pkgs/pubspec_parse/lib/src/pubspec.g.dart
+++ b/pkgs/pubspec_parse/lib/src/pubspec.g.dart
@@ -11,6 +11,7 @@
     final val = Pubspec($checkedConvert(json, 'name', (v) => v as String),
         version: $checkedConvert(json, 'version',
             (v) => v == null ? null : _versionFromString(v as String)),
+        publishTo: $checkedConvert(json, 'publish_to', (v) => v as String),
         author: $checkedConvert(json, 'author', (v) => v as String),
         authors: $checkedConvert(json, 'authors',
             (v) => (v as List)?.map((e) => e as String)?.toList()),
@@ -28,6 +29,7 @@
             json, 'dependency_overrides', (v) => parseDeps(v as Map)));
     return val;
   }, fieldKeyMap: const {
+    'publishTo': 'publish_to',
     'devDependencies': 'dev_dependencies',
     'dependencyOverrides': 'dependency_overrides'
   });
diff --git a/pkgs/pubspec_parse/pubspec.yaml b/pkgs/pubspec_parse/pubspec.yaml
index 390e6014..fde8f8a 100644
--- a/pkgs/pubspec_parse/pubspec.yaml
+++ b/pkgs/pubspec_parse/pubspec.yaml
@@ -2,7 +2,7 @@
 description: >-
   Simple package for parsing pubspec.yaml files with a type-safe API and rich
   error reporting.
-version: 0.1.2+3
+version: 0.1.3-dev
 homepage: https://github.com/dart-lang/pubspec_parse
 author: Dart Team <misc@dartlang.org>
 
diff --git a/pkgs/pubspec_parse/test/parse_test.dart b/pkgs/pubspec_parse/test/parse_test.dart
index df2475b..23d2078 100644
--- a/pkgs/pubspec_parse/test/parse_test.dart
+++ b/pkgs/pubspec_parse/test/parse_test.dart
@@ -12,6 +12,7 @@
     final value = parse({'name': 'sample'});
     expect(value.name, 'sample');
     expect(value.version, isNull);
+    expect(value.publishTo, isNull);
     expect(value.description, isNull);
     expect(value.homepage, isNull);
     // ignore: deprecated_member_use
@@ -30,6 +31,7 @@
     final value = parse({
       'name': 'sample',
       'version': version.toString(),
+      'publish_to': 'none',
       'author': 'name@example.com',
       'environment': {'sdk': sdkConstraint.toString()},
       'description': 'description',
@@ -38,6 +40,7 @@
     });
     expect(value.name, 'sample');
     expect(value.version, version);
+    expect(value.publishTo, 'none');
     expect(value.description, 'description');
     expect(value.homepage, 'homepage');
     // ignore: deprecated_member_use
@@ -61,6 +64,47 @@
     expect(value.environment, containsPair('sdk', isNull));
   });
 
+  group('publish_to', () {
+    for (var entry in {
+      42: r'''
+line 3, column 16: Unsupported value for `publish_to`.
+ "publish_to": 42
+               ^^^''',
+      '##not a uri!': r'''
+line 3, column 16: must be an http or https URL.
+ "publish_to": "##not a uri!"
+               ^^^^^^^^^^^^^^''',
+      '/cool/beans': r'''
+line 3, column 16: must be an http or https URL.
+ "publish_to": "/cool/beans"
+               ^^^^^^^^^^^^^''',
+      'file:///Users/kevmoo/': r'''
+line 3, column 16: must be an http or https URL.
+ "publish_to": "file:///Users/kevmoo/"
+               ^^^^^^^^^^^^^^^^^^^^^^^'''
+    }.entries) {
+      test('cannot be `${entry.key}`', () {
+        expectParseThrows(
+          {'name': 'sample', 'publish_to': entry.key},
+          entry.value,
+          skipTryPub: true,
+        );
+      });
+    }
+
+    for (var entry in {
+      null: null,
+      'http': 'http://example.com',
+      'https': 'https://example.com',
+      'none': 'none'
+    }.entries) {
+      test('can be ${entry.key}', () {
+        final value = parse({'name': 'sample', 'publish_to': entry.value});
+        expect(value.publishTo, entry.value);
+      });
+    }
+  });
+
   group('author, authors', () {
     test('one author', () {
       final value = parse({'name': 'sample', 'author': 'name@example.com'});
diff --git a/pkgs/pubspec_parse/test/test_utils.dart b/pkgs/pubspec_parse/test/test_utils.dart
index 9750bfa..491873f 100644
--- a/pkgs/pubspec_parse/test/test_utils.dart
+++ b/pkgs/pubspec_parse/test/test_utils.dart
@@ -89,6 +89,10 @@
   }
 }
 
-void expectParseThrows(Object content, String expectedError) => expect(
-    () => parse(content, quietOnError: true),
-    _throwsParsedYamlException(expectedError));
+void expectParseThrows(
+  Object content,
+  String expectedError, {
+  bool skipTryPub = false,
+}) =>
+    expect(() => parse(content, quietOnError: true, skipTryPub: skipTryPub),
+        _throwsParsedYamlException(expectedError));