From f072c825542c8e4163e4e385d5197581221d1093 Mon Sep 17 00:00:00 2001 From: Dhruv Maradiya Date: Fri, 31 Jan 2025 00:16:46 +0530 Subject: [PATCH 1/4] feat: added toJson functionality --- pkgs/pubspec_parse/lib/src/dependency.dart | 36 +++++++++++++++++++++- pkgs/pubspec_parse/lib/src/pubspec.dart | 31 ++++++++++++++----- pkgs/pubspec_parse/lib/src/pubspec.g.dart | 25 +++++++++++++++ pkgs/pubspec_parse/lib/src/screenshot.dart | 8 +++++ 4 files changed, 91 insertions(+), 9 deletions(-) diff --git a/pkgs/pubspec_parse/lib/src/dependency.dart b/pkgs/pubspec_parse/lib/src/dependency.dart index 24c65eac1..8e0d0c0ed 100644 --- a/pkgs/pubspec_parse/lib/src/dependency.dart +++ b/pkgs/pubspec_parse/lib/src/dependency.dart @@ -90,7 +90,9 @@ Dependency? _fromJson(Object? data, String name) { return null; } -sealed class Dependency {} +sealed class Dependency { + Map toJson(); +} @JsonSerializable() class SdkDependency extends Dependency { @@ -110,6 +112,12 @@ class SdkDependency extends Dependency { @override String toString() => 'SdkDependency: $sdk'; + + @override + Map toJson() => { + 'sdk': sdk, + 'version': version.toString(), + }; } @JsonSerializable() @@ -145,6 +153,15 @@ class GitDependency extends Dependency { @override String toString() => 'GitDependency: url@$url'; + + @override + Map toJson() => { + 'git': { + 'url': url.toString(), + if (ref != null) 'ref': ref, + if (path != null) 'path': path, + }, + }; } Uri? parseGitUriOrNull(String? value) => @@ -204,6 +221,9 @@ class PathDependency extends Dependency { @override String toString() => 'PathDependency: path@$path'; + + @override + Map toJson() => {'path': path}; } @JsonSerializable(disallowUnrecognizedKeys: true) @@ -228,6 +248,12 @@ class HostedDependency extends Dependency { @override String toString() => 'HostedDependency: $version'; + + @override + Map toJson() => { + 'version': version.toString(), + if (hosted != null) 'hosted': hosted!.toJson(), + }; } @JsonSerializable(disallowUnrecognizedKeys: true) @@ -271,7 +297,15 @@ class HostedDetails { @override int get hashCode => Object.hash(name, url); + + Map toJson() => { + if (declaredName != null) 'name': declaredName, + 'url': url.toString(), + }; } VersionConstraint _constraintFromString(String? input) => input == null ? VersionConstraint.any : VersionConstraint.parse(input); + +Map serializeDeps(Map input) => + input.map((k, v) => MapEntry(k, v.toJson())); diff --git a/pkgs/pubspec_parse/lib/src/pubspec.dart b/pkgs/pubspec_parse/lib/src/pubspec.dart index eb779089a..01a1a0d39 100644 --- a/pkgs/pubspec_parse/lib/src/pubspec.dart +++ b/pkgs/pubspec_parse/lib/src/pubspec.dart @@ -11,13 +11,13 @@ import 'screenshot.dart'; part 'pubspec.g.dart'; -@JsonSerializable() +@JsonSerializable(createToJson: true) class Pubspec { // TODO: executables final String name; - @JsonKey(fromJson: _versionFromString) + @JsonKey(fromJson: _versionFromString, toJson: _versionToString) final Version? version; final String? description; @@ -51,7 +51,7 @@ class Pubspec { final List? ignoredAdvisories; /// Optional field for specifying included screenshot files. - @JsonKey(fromJson: parseScreenshots) + @JsonKey(fromJson: parseScreenshots, toJson: serializeScreenshots) final List? screenshots; /// If there is exactly 1 value in [authors], returns it. @@ -73,16 +73,16 @@ class Pubspec { final List authors; final String? documentation; - @JsonKey(fromJson: _environmentMap) + @JsonKey(fromJson: _environmentMap, toJson: _serializeEnvironment) final Map environment; - @JsonKey(fromJson: parseDeps) + @JsonKey(fromJson: parseDeps, toJson: serializeDeps) final Map dependencies; - @JsonKey(fromJson: parseDeps) + @JsonKey(fromJson: parseDeps, toJson: serializeDeps) final Map devDependencies; - @JsonKey(fromJson: parseDeps) + @JsonKey(fromJson: parseDeps, toJson: serializeDeps) final Map dependencyOverrides; /// Optional configuration specific to [Flutter](https://flutter.io/) @@ -94,7 +94,7 @@ class Pubspec { final Map? flutter; /// Optional field to specify executables - @JsonKey(fromJson: _executablesMap) + @JsonKey(fromJson: _executablesMap, toJson: _serializeExecutables) final Map executables; /// If this package is a Pub Workspace, this field lists the sub-packages. @@ -177,6 +177,9 @@ class Pubspec { return _$PubspecFromJson(json); } + // Returns a JSON-serializable map for this instance. + Map toJson() => _$PubspecToJson(this); + /// Parses source [yaml] into [Pubspec]. /// /// When [lenient] is set, top-level property-parsing or type cast errors are @@ -256,3 +259,15 @@ Map _executablesMap(Map? source) => } }) ?? {}; + +Map _serializeEnvironment( + Map map, +) => + map.map( + (key, value) => MapEntry(key, value?.toString()), + ); + +String? _versionToString(Version? version) => version?.toString(); + +Map _serializeExecutables(Map? map) => + map?.map(MapEntry.new) ?? {}; diff --git a/pkgs/pubspec_parse/lib/src/pubspec.g.dart b/pkgs/pubspec_parse/lib/src/pubspec.g.dart index 58e015a5e..96b1af203 100644 --- a/pkgs/pubspec_parse/lib/src/pubspec.g.dart +++ b/pkgs/pubspec_parse/lib/src/pubspec.g.dart @@ -67,3 +67,28 @@ Pubspec _$PubspecFromJson(Map json) => $checkedCreate( 'dependencyOverrides': 'dependency_overrides' }, ); + +Map _$PubspecToJson(Pubspec instance) => { + 'name': instance.name, + 'version': _versionToString(instance.version), + 'description': instance.description, + 'homepage': instance.homepage, + 'publish_to': instance.publishTo, + 'repository': instance.repository?.toString(), + 'issue_tracker': instance.issueTracker?.toString(), + 'funding': instance.funding?.map((e) => e.toString()).toList(), + 'topics': instance.topics, + 'ignored_advisories': instance.ignoredAdvisories, + 'screenshots': serializeScreenshots(instance.screenshots), + 'author': instance.author, + 'authors': instance.authors, + 'documentation': instance.documentation, + 'environment': _serializeEnvironment(instance.environment), + 'dependencies': serializeDeps(instance.dependencies), + 'dev_dependencies': serializeDeps(instance.devDependencies), + 'dependency_overrides': serializeDeps(instance.dependencyOverrides), + 'flutter': instance.flutter, + 'executables': _serializeExecutables(instance.executables), + 'workspace': instance.workspace, + 'resolution': instance.resolution, + }; diff --git a/pkgs/pubspec_parse/lib/src/screenshot.dart b/pkgs/pubspec_parse/lib/src/screenshot.dart index f5f0be2ea..1ca861367 100644 --- a/pkgs/pubspec_parse/lib/src/screenshot.dart +++ b/pkgs/pubspec_parse/lib/src/screenshot.dart @@ -63,3 +63,11 @@ List parseScreenshots(List? input) { } return res; } + +List> serializeScreenshots(List? input) => + input + ?.map( + (e) => {'description': e.description, 'path': e.path}, + ) + .toList() ?? + []; From 5170f1d3525778582b2031d819424fb33bcb4905 Mon Sep 17 00:00:00 2001 From: Dhruv Maradiya Date: Fri, 31 Jan 2025 00:16:54 +0530 Subject: [PATCH 2/4] test: add unit tests for Pubspec toJson functionality --- pkgs/pubspec_parse/test/tojson_test.dart | 308 +++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 pkgs/pubspec_parse/test/tojson_test.dart diff --git a/pkgs/pubspec_parse/test/tojson_test.dart b/pkgs/pubspec_parse/test/tojson_test.dart new file mode 100644 index 000000000..eb9161f15 --- /dev/null +++ b/pkgs/pubspec_parse/test/tojson_test.dart @@ -0,0 +1,308 @@ +// Copyright (c) 2018, 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. + +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:test/test.dart'; + +import 'test_utils.dart'; + +void main() { + group('Pubspec toJson tests', () { + test('minimal set values', () async { + final value = await parse(defaultPubspec); + final jsonValue = value.toJson(); + + expect(jsonValue['name'], 'sample'); + expect(jsonValue['version'], isNull); + expect(jsonValue['publishTo'], isNull); + expect(jsonValue['description'], isNull); + expect(jsonValue['homepage'], isNull); + expect(jsonValue['author'], isNull); + expect(jsonValue['authors'], isEmpty); + expect(jsonValue['environment'], {'sdk': '>=2.12.0 <3.0.0'}); + expect(jsonValue['documentation'], isNull); + expect(jsonValue['dependencies'], isEmpty); + expect(jsonValue['dev_dependencies'], isEmpty); + expect(jsonValue['dependency_overrides'], isEmpty); + expect(jsonValue['flutter'], isNull); + expect(jsonValue['repository'], isNull); + expect(jsonValue['issue_tracker'], isNull); + expect(jsonValue['screenshots'], isEmpty); + expect(jsonValue['workspace'], isNull); + expect(jsonValue['resolution'], isNull); + expect(jsonValue['executables'], isEmpty); + }); + + test('all fields set', () async { + final version = Version.parse('1.2.3'); + final sdkConstraint = VersionConstraint.parse('>=3.6.0 <4.0.0'); + + final value = await parse( + { + 'name': 'sample', + 'version': version.toString(), + 'publish_to': 'none', + 'author': 'name@example.com', + 'environment': {'sdk': sdkConstraint.toString()}, + 'description': 'description', + 'homepage': 'homepage', + 'documentation': 'documentation', + 'repository': 'https://github.com/example/repo', + 'issue_tracker': 'https://github.com/example/repo/issues', + 'funding': ['https://patreon.com/example'], + 'topics': ['widget', 'button'], + 'ignored_advisories': ['111', '222'], + 'screenshots': [ + {'description': 'my screenshot', 'path': 'path/to/screenshot'}, + ], + 'workspace': ['pkg1', 'pkg2'], + 'resolution': 'workspace', + 'executables': { + 'my_script': 'bin/my_script.dart', + 'my_script2': 'bin/my_script2.dart', + }, + 'dependencies': {'foo': '1.0.0'}, + 'dev_dependencies': {'bar': '2.0.0'}, + 'dependency_overrides': {'baz': '3.0.0'}, + }, + skipTryPub: true, + ); + + final jsonValue = value.toJson(); + + expect(jsonValue['name'], 'sample'); + expect(jsonValue['version'], version.toString()); + expect(jsonValue['publish_to'], 'none'); + expect(jsonValue['description'], 'description'); + expect(jsonValue['homepage'], 'homepage'); + expect(jsonValue['author'], 'name@example.com'); + expect(jsonValue['authors'], ['name@example.com']); + expect( + jsonValue['environment'], + containsPair('sdk', sdkConstraint.toString()), + ); + expect(jsonValue['documentation'], 'documentation'); + expect(jsonValue['dependencies'], hasLength(1)); + expect( + jsonValue['dependencies'], + containsPair('foo', {'version': '1.0.0'}), + ); + expect(jsonValue['dev_dependencies'], hasLength(1)); + expect( + jsonValue['dev_dependencies'], + containsPair('bar', { + 'version': '2.0.0', + }), + ); + expect(jsonValue['dependency_overrides'], hasLength(1)); + expect( + jsonValue['dependency_overrides'], + containsPair('baz', { + 'version': '3.0.0', + }), + ); + expect(jsonValue['repository'], 'https://github.com/example/repo'); + expect( + jsonValue['issue_tracker'], + 'https://github.com/example/repo/issues', + ); + expect(jsonValue['funding'], ['https://patreon.com/example']); + expect(jsonValue['topics'], ['widget', 'button']); + expect(jsonValue['ignored_advisories'], ['111', '222']); + expect(jsonValue['screenshots'], [ + {'description': 'my screenshot', 'path': 'path/to/screenshot'}, + ]); + expect(jsonValue['workspace'], ['pkg1', 'pkg2']); + expect(jsonValue['resolution'], 'workspace'); + expect(jsonValue['executables'], { + 'my_script': 'bin/my_script.dart', + 'my_script2': 'bin/my_script2.dart', + }); + }); + }); + + group('Pubspec round trip tests', () { + test('minimal set values', () async { + final value = await parse(defaultPubspec); + final jsonValue = value.toJson(); + final newValue = Pubspec.fromJson(jsonValue); + + expect(newValue.name, value.name); + expect(newValue.version, value.version); + expect(newValue.publishTo, value.publishTo); + expect(newValue.description, value.description); + expect(newValue.homepage, value.homepage); + expect(newValue.author, value.author); + expect(newValue.authors, value.authors); + expect(newValue.environment, value.environment); + expect(newValue.documentation, value.documentation); + expect(newValue.dependencies, value.dependencies); + expect(newValue.devDependencies, value.devDependencies); + expect(newValue.dependencyOverrides, value.dependencyOverrides); + expect(newValue.flutter, value.flutter); + expect(newValue.repository, value.repository); + expect(newValue.issueTracker, value.issueTracker); + expect(newValue.screenshots, value.screenshots); + expect(newValue.workspace, value.workspace); + expect(newValue.resolution, value.resolution); + expect(newValue.executables, value.executables); + }); + + test('all fields set', () async { + final version = Version.parse('1.2.3'); + final sdkConstraint = VersionConstraint.parse('>=3.6.0 <4.0.0'); + final value = await parse( + { + 'name': 'sample', + 'version': version.toString(), + 'publish_to': 'none', + 'author': 'name@example.com', + 'environment': {'sdk': sdkConstraint.toString()}, + 'description': 'description', + 'homepage': 'homepage', + 'documentation': 'documentation', + 'repository': 'https://github.com/example/repo', + 'issue_tracker': 'https://github.com/example/repo/issues', + 'funding': ['https://patreon.com/example'], + 'topics': ['widget', 'button'], + 'ignored_advisories': ['111', '222'], + 'screenshots': [ + {'description': 'my screenshot', 'path': 'path/to/screenshot'}, + ], + 'workspace': ['pkg1', 'pkg2'], + 'resolution': 'workspace', + 'executables': { + 'my_script': 'bin/my_script.dart', + 'my_script2': 'bin/my_script2.dart', + }, + 'dependencies': {'foo': '1.0.0'}, + 'dev_dependencies': {'bar': '2.0.0'}, + 'dependency_overrides': {'baz': '3.0.0'}, + }, + skipTryPub: true, + ); + + final jsonValue = value.toJson(); + + final newValue = Pubspec.fromJson(jsonValue); + + expect(newValue.name, value.name); + expect(newValue.version, value.version); + expect(newValue.publishTo, value.publishTo); + expect(newValue.description, value.description); + expect(newValue.homepage, value.homepage); + expect(newValue.author, value.author); + expect(newValue.authors, value.authors); + expect(newValue.environment, value.environment); + expect(newValue.documentation, value.documentation); + expect(newValue.dependencies, value.dependencies); + expect(newValue.devDependencies, value.devDependencies); + expect(newValue.dependencyOverrides, value.dependencyOverrides); + expect(newValue.flutter, value.flutter); + expect(newValue.repository, value.repository); + expect(newValue.issueTracker, value.issueTracker); + expect(newValue.screenshots?.length, value.screenshots?.length); + expect( + newValue.screenshots?.first.description, + value.screenshots?.first.description, + ); + expect(newValue.screenshots?.first.path, value.screenshots?.first.path); + expect(newValue.workspace, value.workspace); + expect(newValue.resolution, value.resolution); + expect(newValue.executables, value.executables); + }); + + test('dependencies', () async { + final value = await parse( + { + ...defaultPubspec, + 'dependencies': { + 'flutter': { + 'sdk': 'flutter', + }, + 'http': '^1.1.0', + 'provider': { + 'version': '^6.0.5', + }, + 'firebase_core': { + 'hosted': { + 'name': 'firebase_core', + 'url': 'https://pub.dev', + }, + 'version': '^2.13.0', + }, + 'google_fonts': { + 'sdk': 'flutter', + 'version': '^4.0.3', + }, + 'flutter_bloc': { + 'git': 'https://github.com/felangel/bloc.git', + }, + 'shared_preferences': { + 'git': { + 'url': 'https://github.com/flutter/plugins.git', + 'ref': 'main', + 'path': 'packages/shared_preferences/shared_preferences', + }, + }, + 'local_utils': { + 'path': '../local_utils', + }, + }, + }, + skipTryPub: true, + ); + + final jsonValue = value.toJson(); + + final newValue = await parse(jsonValue, skipTryPub: true); + + expect(value.dependencies, hasLength(8)); + expect(value.dependencies['flutter'], isA()); + expect(value.dependencies['http'], isA()); + expect(value.dependencies['provider'], isA()); + expect(value.dependencies['firebase_core'], isA()); + expect(value.dependencies['google_fonts'], isA()); + expect(value.dependencies['flutter_bloc'], isA()); + expect(value.dependencies['shared_preferences'], isA()); + expect(value.dependencies['local_utils'], isA()); + + expect( + value.dependencies['flutter']?.toJson(), + newValue.dependencies['flutter']?.toJson(), + ); + expect( + value.dependencies['http']?.toJson(), + newValue.dependencies['http']?.toJson(), + ); + expect( + value.dependencies['provider']?.toJson(), + newValue.dependencies['provider']?.toJson(), + ); + expect( + value.dependencies['firebase_core']?.toJson(), + newValue.dependencies['firebase_core']?.toJson(), + ); + expect( + value.dependencies['google_fonts']?.toJson(), + newValue.dependencies['google_fonts']?.toJson(), + ); + expect( + value.dependencies['flutter_bloc']?.toJson(), + newValue.dependencies['flutter_bloc']?.toJson(), + ); + expect( + value.dependencies['shared_preferences']?.toJson(), + newValue.dependencies['shared_preferences']?.toJson(), + ); + expect( + value.dependencies['local_utils']?.toJson(), + newValue.dependencies['local_utils']?.toJson(), + ); + }); + }); +} From 68a12a639da8f5731af2ebc0f246d7284c33c190 Mon Sep 17 00:00:00 2001 From: Dhruv Maradiya Date: Fri, 31 Jan 2025 00:25:14 +0530 Subject: [PATCH 3/4] chore: update changelog and version to 1.5.1 --- pkgs/pubspec_parse/CHANGELOG.md | 4 ++++ pkgs/pubspec_parse/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pkgs/pubspec_parse/CHANGELOG.md b/pkgs/pubspec_parse/CHANGELOG.md index 5aeb49802..a83135808 100644 --- a/pkgs/pubspec_parse/CHANGELOG.md +++ b/pkgs/pubspec_parse/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.5.1 + +- Added `toJson` method to `Pubspec` to serialize the object back to a `Map`. + ## 1.5.0 - Added fields to `Pubspec`: `executables`, `resolution`, `workspace`. diff --git a/pkgs/pubspec_parse/pubspec.yaml b/pkgs/pubspec_parse/pubspec.yaml index 90741efff..eb9b35853 100644 --- a/pkgs/pubspec_parse/pubspec.yaml +++ b/pkgs/pubspec_parse/pubspec.yaml @@ -1,5 +1,5 @@ name: pubspec_parse -version: 1.5.0 +version: 1.5.1 description: >- Simple package for parsing pubspec.yaml files with a type-safe API and rich error reporting. From a22391e67a87a0ba67d865254d06e71411ed505b Mon Sep 17 00:00:00 2001 From: Dhruv Maradiya Date: Fri, 31 Jan 2025 10:09:53 +0530 Subject: [PATCH 4/4] update version to 1.6.0-wip in changelog and pubspec.yaml --- pkgs/pubspec_parse/CHANGELOG.md | 2 +- pkgs/pubspec_parse/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/pubspec_parse/CHANGELOG.md b/pkgs/pubspec_parse/CHANGELOG.md index a83135808..cc5c06b16 100644 --- a/pkgs/pubspec_parse/CHANGELOG.md +++ b/pkgs/pubspec_parse/CHANGELOG.md @@ -1,4 +1,4 @@ -## 1.5.1 +## 1.6.0-wip - Added `toJson` method to `Pubspec` to serialize the object back to a `Map`. diff --git a/pkgs/pubspec_parse/pubspec.yaml b/pkgs/pubspec_parse/pubspec.yaml index eb9b35853..4a5f4568f 100644 --- a/pkgs/pubspec_parse/pubspec.yaml +++ b/pkgs/pubspec_parse/pubspec.yaml @@ -1,5 +1,5 @@ name: pubspec_parse -version: 1.5.1 +version: 1.6.0-wip description: >- Simple package for parsing pubspec.yaml files with a type-safe API and rich error reporting.