Add support for 'screenshots' field (dart-lang/pubspec_parse#78)

diff --git a/pkgs/pubspec_parse/CHANGELOG.md b/pkgs/pubspec_parse/CHANGELOG.md
index 8f2d288..5f3d5d6 100644
--- a/pkgs/pubspec_parse/CHANGELOG.md
+++ b/pkgs/pubspec_parse/CHANGELOG.md
@@ -1,14 +1,12 @@
-## 1.2.0-dev
+## 1.2.0
 
+- Added support for `screenshots` field.
 - Update `HostedDetails` to reflect how `hosted` dependencies are parsed in
   Dart 2.15:
    - Add `HostedDetails.declaredName` as the (optional) `name` property in a 
      `hosted` block.
    - `HostedDetails.name` now falls back to the name of the dependency if no
       name is declared in the block.
-
-## 1.1.1-dev
-
 - Require Dart SDK >= 2.14.0
 
 ## 1.1.0
diff --git a/pkgs/pubspec_parse/lib/pubspec_parse.dart b/pkgs/pubspec_parse/lib/pubspec_parse.dart
index 63c1e30..7360d1a 100644
--- a/pkgs/pubspec_parse/lib/pubspec_parse.dart
+++ b/pkgs/pubspec_parse/lib/pubspec_parse.dart
@@ -11,3 +11,4 @@
         SdkDependency,
         PathDependency;
 export 'src/pubspec.dart' show Pubspec;
+export 'src/screenshot.dart' show Screenshot;
diff --git a/pkgs/pubspec_parse/lib/src/pubspec.dart b/pkgs/pubspec_parse/lib/src/pubspec.dart
index 8cc7b09..82c78aa 100644
--- a/pkgs/pubspec_parse/lib/src/pubspec.dart
+++ b/pkgs/pubspec_parse/lib/src/pubspec.dart
@@ -7,6 +7,7 @@
 import 'package:pub_semver/pub_semver.dart';
 
 import 'dependency.dart';
+import 'screenshot.dart';
 
 part 'pubspec.g.dart';
 
@@ -39,6 +40,10 @@
   /// view existing ones.
   final Uri? issueTracker;
 
+  /// Optional field for specifying included screenshot files.
+  @JsonKey(fromJson: parseScreenshots)
+  final List<Screenshot>? screenshots;
+
   /// If there is exactly 1 value in [authors], returns it.
   ///
   /// If there are 0 or more than 1, returns `null`.
@@ -96,6 +101,7 @@
     this.homepage,
     this.repository,
     this.issueTracker,
+    this.screenshots,
     this.documentation,
     this.description,
     Map<String, Dependency>? dependencies,
diff --git a/pkgs/pubspec_parse/lib/src/pubspec.g.dart b/pkgs/pubspec_parse/lib/src/pubspec.g.dart
index cadd38e..221cc83 100644
--- a/pkgs/pubspec_parse/lib/src/pubspec.g.dart
+++ b/pkgs/pubspec_parse/lib/src/pubspec.g.dart
@@ -27,6 +27,8 @@
               'repository', (v) => v == null ? null : Uri.parse(v as String)),
           issueTracker: $checkedConvert('issue_tracker',
               (v) => v == null ? null : Uri.parse(v as String)),
+          screenshots: $checkedConvert(
+              'screenshots', (v) => parseScreenshots(v as List?)),
           documentation: $checkedConvert('documentation', (v) => v as String?),
           description: $checkedConvert('description', (v) => v as String?),
           dependencies:
diff --git a/pkgs/pubspec_parse/lib/src/screenshot.dart b/pkgs/pubspec_parse/lib/src/screenshot.dart
new file mode 100644
index 0000000..f5f0be2
--- /dev/null
+++ b/pkgs/pubspec_parse/lib/src/screenshot.dart
@@ -0,0 +1,65 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:json_annotation/json_annotation.dart';
+
+@JsonSerializable()
+class Screenshot {
+  final String description;
+  final String path;
+
+  Screenshot(this.description, this.path);
+}
+
+List<Screenshot> parseScreenshots(List? input) {
+  final res = <Screenshot>[];
+  if (input == null) {
+    return res;
+  }
+
+  for (final e in input) {
+    if (e is! Map) continue;
+
+    final description = e['description'];
+    if (description == null) {
+      throw CheckedFromJsonException(
+        e,
+        'description',
+        'Screenshot',
+        'Missing required key `description`',
+      );
+    }
+
+    if (description is! String) {
+      throw CheckedFromJsonException(
+        e,
+        'description',
+        'Screenshot',
+        '`$description` is not a String',
+      );
+    }
+
+    final path = e['path'];
+    if (path == null) {
+      throw CheckedFromJsonException(
+        e,
+        'path',
+        'Screenshot',
+        'Missing required key `path`',
+      );
+    }
+
+    if (path is! String) {
+      throw CheckedFromJsonException(
+        e,
+        'path',
+        'Screenshot',
+        '`$path` is not a String',
+      );
+    }
+
+    res.add(Screenshot(description, path));
+  }
+  return res;
+}
diff --git a/pkgs/pubspec_parse/pubspec.yaml b/pkgs/pubspec_parse/pubspec.yaml
index 90ae1fe..ae7a03f 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: 1.2.0-dev
+version: 1.2.0
 repository: https://github.com/dart-lang/pubspec_parse
 
 environment:
diff --git a/pkgs/pubspec_parse/test/parse_test.dart b/pkgs/pubspec_parse/test/parse_test.dart
index 5f70de8..e8d24c6 100644
--- a/pkgs/pubspec_parse/test/parse_test.dart
+++ b/pkgs/pubspec_parse/test/parse_test.dart
@@ -31,6 +31,7 @@
     expect(value.flutter, isNull);
     expect(value.repository, isNull);
     expect(value.issueTracker, isNull);
+    expect(value.screenshots, isEmpty);
   });
 
   test('all fields set', () {
@@ -47,6 +48,9 @@
       'documentation': 'documentation',
       'repository': 'https://github.com/example/repo',
       'issue_tracker': 'https://github.com/example/repo/issues',
+      'screenshots': [
+        {'description': 'my screenshot', 'path': 'path/to/screenshot'}
+      ],
     });
     expect(value.name, 'sample');
     expect(value.version, version);
@@ -66,6 +70,9 @@
       value.issueTracker,
       Uri.parse('https://github.com/example/repo/issues'),
     );
+    expect(value.screenshots, hasLength(1));
+    expect(value.screenshots!.first.description, 'my screenshot');
+    expect(value.screenshots!.first.path, 'path/to/screenshot');
   });
 
   test('environment values can be null', () {
@@ -357,6 +364,160 @@
     });
   });
 
+  group('screenshots', () {
+    test('one screenshot', () {
+      final value = parse({
+        ...defaultPubspec,
+        'screenshots': [
+          {'description': 'my screenshot', 'path': 'path/to/screenshot'}
+        ],
+      });
+      expect(value.screenshots, hasLength(1));
+      expect(value.screenshots!.first.description, 'my screenshot');
+      expect(value.screenshots!.first.path, 'path/to/screenshot');
+    });
+
+    test('many screenshots', () {
+      final value = parse({
+        ...defaultPubspec,
+        'screenshots': [
+          {'description': 'my screenshot', 'path': 'path/to/screenshot'},
+          {
+            'description': 'my second screenshot',
+            'path': 'path/to/screenshot2'
+          },
+        ],
+      });
+      expect(value.screenshots, hasLength(2));
+      expect(value.screenshots!.first.description, 'my screenshot');
+      expect(value.screenshots!.first.path, 'path/to/screenshot');
+      expect(value.screenshots!.last.description, 'my second screenshot');
+      expect(value.screenshots!.last.path, 'path/to/screenshot2');
+    });
+
+    test('one screenshot plus invalid entries', () {
+      final value = parse({
+        ...defaultPubspec,
+        'screenshots': [
+          42,
+          {
+            'description': 'my screenshot',
+            'path': 'path/to/screenshot',
+            'extraKey': 'not important'
+          },
+          'not a screenshot',
+        ],
+      });
+      expect(value.screenshots, hasLength(1));
+      expect(value.screenshots!.first.description, 'my screenshot');
+      expect(value.screenshots!.first.path, 'path/to/screenshot');
+    });
+
+    test('invalid entries', () {
+      final value = parse({
+        ...defaultPubspec,
+        'screenshots': [
+          42,
+          'not a screenshot',
+        ],
+      });
+      expect(value.screenshots, isEmpty);
+    });
+
+    test('missing key `dessription', () {
+      expectParseThrows(
+        {
+          ...defaultPubspec,
+          'screenshots': [
+            {'path': 'my/path'},
+          ],
+        },
+        r'''
+line 7, column 3: Missing key "description". Missing required key `description`
+  ╷
+7 │ ┌   {
+8 │ │    "path": "my/path"
+9 │ └   }
+  ╵''',
+        skipTryPub: true,
+      );
+    });
+
+    test('missing key `path`', () {
+      expectParseThrows(
+        {
+          ...defaultPubspec,
+          'screenshots': [
+            {'description': 'my screenshot'},
+          ],
+        },
+        r'''
+line 7, column 3: Missing key "path". Missing required key `path`
+  ╷
+7 │ ┌   {
+8 │ │    "description": "my screenshot"
+9 │ └   }
+  ╵''',
+        skipTryPub: true,
+      );
+    });
+
+    test('Value of description not a String`', () {
+      expectParseThrows(
+        {
+          ...defaultPubspec,
+          'screenshots': [
+            {'description': 42},
+          ],
+        },
+        r'''
+line 8, column 19: Unsupported value for "description". `42` is not a String
+  ╷
+8 │      "description": 42
+  │ ┌───────────────────^
+9 │ │   }
+  │ └──^
+  ╵''',
+        skipTryPub: true,
+      );
+    });
+
+    test('Value of path not a String`', () {
+      expectParseThrows(
+        {
+          ...defaultPubspec,
+          'screenshots': [
+            {
+              'description': '',
+              'path': 42,
+            },
+          ],
+        },
+        r'''
+line 9, column 12: Unsupported value for "path". `42` is not a String
+   ╷
+9  │      "path": 42
+   │ ┌────────────^
+10 │ │   }
+   │ └──^
+   ╵''',
+        skipTryPub: true,
+      );
+    });
+
+    test('invalid screenshot - lenient', () {
+      final value = parse(
+        {
+          ...defaultPubspec,
+          'screenshots': 'Invalid value',
+        },
+        lenient: true,
+      );
+      expect(value.name, 'sample');
+      expect(value.screenshots, isEmpty);
+    });
+  });
+
   group('lenient', () {
     test('null', () {
       expectParseThrows(