diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 70fa0fc..c32958f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,32 +1,14 @@ -# Do not run workflow for now -name: Publish +name: Publish to pub.dev + on: - release: - types: [published] + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+' jobs: publish: - runs-on: ubuntu-latest - - container: - image: dart:latest - - steps: - - uses: actions/checkout@v2 - - name: Install dependencies - run: dart pub get - - name: Install Cider - run: dart pub global activate cider - - name: Set new version - run: dart pub global run cider version ${{ github.event.release.tag_name }} - - name: Set a new release - run: dart pub global run cider release - - name: Build Runner - run: dart pub run build_runner build --delete-conflicting-outputs - - name: Run tests - run: dart pub run test - - name: Publish Dart/Flutter package - uses: sakebook/actions-flutter-pub-publisher@v1.3.1 - with: - credential: ${{ secrets.PUB_CREDENTIALS }} - flutter_package: false + permissions: + id-token: write + uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 + with: + environment: pub.dev diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 102d4a8..5f2fe67 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,37 +6,22 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest container: image: dart:latest - env: - PUB_CREDENTIALS: ${{ secrets.PUB_CREDENTIALS }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install dependencies run: dart pub get - # Builder - name: Build Runner - run: dart pub run build_runner build --delete-conflicting-outputs - # Run test + run: dart run build_runner build + - name: Analyze + run: dart analyze - name: Run tests - run: dart pub run test - # # Get coverage - # - name: Test Coverage - # run: pub run test_cov - # # Activate remove coverage - # - name: Remove Coverage activate - # run: pub global activate remove_from_coverage - # # Remove build files from coverage report - # - name: Remove generated files from Coverage - # run: pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '.g.dart$' - # - name: Remove freezed files from Coverage - # run: pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '.freezed.dart$' - # # Upload coverage report - # - name: Coveralls GitHub Action - # uses: coverallsapp/github-action@v1.1.2 - # with: - # github-token: ${{ secrets.GITHUB_TOKEN }} + run: dart test diff --git a/CHANGELOG.md b/CHANGELOG.md index 0048c4f..ea6d3bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,60 @@ +## 4.0.0 + +### Breaking changes + +- Added `SearchOrder.trending` for pub.dev's trend-score ordering. Consumers + with an exhaustive switch over `SearchOrder` must add a `trending` case + +### Added + +- `packageLikes` — public like count of a package, no authentication required + (`GET /api/packages//likes`) +- `publisherInfo` — public publisher profile + (`GET /api/publishers/`) +- `packageVersionScore` — score of a single package version + (`GET /api/packages//versions//score`) +- `packageVersionOptions` — retraction status of a single package version + (`GET /api/packages//versions//options`) +- `topicNameCompletion` — every topic mapped to its package count + (`GET /api/topic-name-completion-data`) +- `SearchResults.message`, set when pub.dev could not fully honour the query +- `PackageScoreCard.weeklyVersionDownloads`, `WeeklyVersionDownloads`, and + `VersionRangeWeeklyDownloads` — total weekly downloads plus major, minor, + and patch version-range histories + [#78](https://github.com/leoafarias/pub_api_client/pull/78) +- `Repository.fromMap` and `Repository.fromJson`, matching the other models +- Re-export the `pubspec_parse` dependency types (`Dependency`, + `HostedDependency`, `GitDependency`, `PathDependency`, `SdkDependency`, + `HostedDetails`, `Pubspec`) so consumers can name the types reachable from + `PubPackage.latestPubspec` + [#78](https://github.com/leoafarias/pub_api_client/pull/78) + +### Fixed + +- Search queries are now percent-encoded. Previously a `#` or `&` in the query + or in a tag silently truncated the request, so searches like `c# & c++` + returned results for `c` +- `fetchGooglePackages` no longer sends a malformed `tools.dart.dev ` publisher + tag, and now includes the `labs.dart.dev` publisher + +### Deprecated + +- `SearchOrder.popularity` — pub.dev retired the popularity score and serves + these requests as `SearchOrder.top`. Use `SearchOrder.downloads` +- `PackageScore.popularityScore` — pub.dev no longer returns this field. Use + `PackageScore.downloadCount30Days` +- `LatestVersion` — unreachable model; no endpoint returns it and no client + method produces it. Scheduled for removal in 5.0.0 + +### Changed + +- Documented that search paging is capped at 10 pages (100 results) by + pub.dev. The cap applies once to `fetchAllPackages`, + `fetchPublisherPackages`, and `fetchFlutterFavorites`, and separately for + each publisher queried by `fetchGooglePackages` +- Generated `*.mapper.dart` files are excluded from analysis, and three lint + rules removed from the Dart SDK were dropped from `analysis_options.yaml` + ## 3.2.0 - Expose discontinued/advisory metadata on `PubPackage` and drop the large cached fixture from version control [#75](https://github.com/leoafarias/pub_api_client/pull/75) diff --git a/README.md b/README.md index d5660b8..09c3695 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,15 @@ drawing -[![Pub Version](https://img.shields.io/pub/v/pub_api_client?label=version&style=flat-square)](https://pub.dev/packages/pub_api_client/changelog) ![Pub Likes](https://img.shields.io/pub/likes/pub_api_client?label=Pub%20Likes&style=fflat-squaree) -![Pub Points](https://img.shields.io/pub/points/pub_api_client?label=Pub%20Points&style=fflat-squaree) [![Coverage Status](https://coveralls.io/repos/github/leoafarias/pub_api_client/badge.svg?branch=main)](https://coveralls.io/github/leoafarias/pub_api_client?branch=main) [![MIT Licence](https://img.shields.io/github/license/leoafarias/pub_api_client?style=flat-square&longCache=true)](https://opensource.org/licenses/mit-license.php) +[![Pub Version](https://img.shields.io/pub/v/pub_api_client?label=version&style=flat-square)](https://pub.dev/packages/pub_api_client/changelog) +![Pub Likes](https://img.shields.io/pub/likes/pub_api_client?label=Pub%20Likes&style=flat-square) +![Pub Points](https://img.shields.io/pub/points/pub_api_client?label=Pub%20Points&style=flat-square) +[![Coverage Status](https://coveralls.io/repos/github/leoafarias/pub_api_client/badge.svg?branch=main)](https://coveralls.io/github/leoafarias/pub_api_client?branch=main) +[![MIT Licence](https://img.shields.io/github/license/leoafarias/pub_api_client?style=flat-square&longCache=true)](https://opensource.org/licenses/mit-license.php) ## An unofficial API client for [Pub.dev](https://www.pub.dev) -Aims to be the most complete and stable pub.dev API client. If any particular endpoint is missing please [open an issue](https://github.com/leoafarias/pub_api_client/issues). +This package aims to be a complete, stable pub.dev API client. If an endpoint +is missing, please [open an issue](https://github.com/leoafarias/pub_api_client/issues). ## Table of contents @@ -17,12 +21,20 @@ Aims to be the most complete and stable pub.dev API client. If any particular en - [Metrics](#get-package-metrics) - [Versions](#get-package-versions) - [Version Info](#get-package-version-info) + - [Version Score](#get-package-version-score) + - [Version Options](#get-package-version-options) - [Publisher](#get-package-publisher) + - [Likes](#get-package-likes) - [Options](#get-package-options) - [Documentation](#get-documentation) + - [Security Advisories](#get-security-advisories) + - [Publishers](#publishers) + - [Publisher Info](#get-publisher-info) - [Search Packages](#search-packages) + - [Sorting search results](#sorting-search-results) - [Paging Search Results](#paging-search-results) - [Like Packages](#like-packages) + - [Completion Data](#completion-data) - [Utilities](#utilities) - [Flutter Favorites](#flutter-favorites) - [Google Packages](#google-packages) @@ -36,8 +48,8 @@ A simple usage example: ```dart import 'package:pub_api_client/pub_api_client.dart'; -main() { - final client = PubClient(); +void main() { + final client = PubClient(); } ``` @@ -47,10 +59,10 @@ main() { #### Get Package Info -Retrieves all available information about an specific package. +Retrieves all available information about a specific package. ```dart -final package = await client.packageInfo('pkg_name'); +final package = await client.packageInfo('pkg_name'); ``` Package pubspec dependencies retain their typed source metadata. The public @@ -62,17 +74,21 @@ SDK names without importing an implementation dependency separately. Returns the following score information about a package. -- Pub Points -- Popularity -- Likes +- Pub Points (`grantedPoints` / `maxPoints`) +- Likes (`likeCount`) +- 30-day downloads (`downloadCount30Days`) +- Tags ```dart -final score = await client.packageScore('pkg_name'); +final score = await client.packageScore('pkg_name'); ``` +> `popularityScore` is always `null` — pub.dev retired the popularity score. +> Use `downloadCount30Days` instead. + #### Get Package Metrics -The method 'packageMetrics' returns the package 'score' together with a 'scorecard' +The `packageMetrics` method returns a package score and scorecard. ```dart final metrics = await client.packageMetrics('pkg_name'); @@ -87,27 +103,56 @@ major, minor, and patch version-range histories. #### Get Package Versions -The method 'packageVersions' also returns the versions. However if all you need is versions use this method since it's lighter. +The `packageVersions` method returns only the published version strings. ```dart -final versions = await client.packageVersions('pkg_name'); +final versions = await client.packageVersions('pkg_name'); ``` #### Get Package Version Info -The method `packageVersionInfo` returns information about a version of a specific package. +The `packageVersionInfo` method returns information about a specific package +version. + +```dart +final version = await client.packageVersionInfo('pkg_name', 'version'); +``` + +#### Get Package Version Score + +The method `packageVersionScore` returns the score of a single version. + +```dart +final score = await client.packageVersionScore('pkg_name', 'version'); +``` + +#### Get Package Version Options + +The method `packageVersionOptions` tells whether a specific version has been +retracted. ```dart -final version = await client.packageVersionInfo('pkg_name', 'version'); +final options = await client.packageVersionOptions('pkg_name', 'version'); +print(options.isRetracted); ``` #### Get Package Publisher -The method `packagePublisher` returns the publisherId of a specific package. +The `packagePublisher` method returns the publisher ID of a specific package. ```dart -final publisher = await client.packagePublisher('pkg_name'); -// publisher.publisherId +final publisher = await client.packagePublisher('pkg_name'); +print(publisher.publisherId); +``` + +#### Get Package Likes + +The method `packageLikes` returns the public like count of a package. Unlike +the [like endpoints](#like-packages) this does not require authentication. + +```dart +final likes = await client.packageLikes('pkg_name'); +print(likes.likes); ``` #### Get Package Options @@ -115,28 +160,53 @@ final publisher = await client.packagePublisher('pkg_name'); The method `packageOptions` returns options of a package. ```dart -final options = await client.packageOptions('pkg_name'); +final options = await client.packageOptions('pkg_name'); ``` #### Get Documentation -The method `documentation` returns all versions and their respective documentation status. +The `documentation` method returns each version and its documentation status. ```dart -final documentation = await client.documentation('pkg_name'); +final documentation = await client.documentation('pkg_name'); +``` + +#### Get Security Advisories +The method `packageAdvisories` returns the security advisories affecting a +package, in [OSV format](https://ossf.github.io/osv-schema/). Returns `null` +when the server does not implement the endpoint. + +```dart +final advisories = await client.packageAdvisories('pkg_name'); +if (advisories != null && advisories.advisories.isNotEmpty) { + print(advisories.advisories.first.pubDisplayUrl); +} +``` + +### Publishers + +#### Get Publisher Info + +The method `publisherInfo` returns the public profile of a publisher. + +```dart +final publisher = await client.publisherInfo('dart.dev'); +print(publisher.description); ``` ### Like Packages -All requests for likes require pub.dev authentication. +The endpoints in this section inspect or change the authenticated user's like +state and require pub.dev authentication. The package-wide count returned by +`packageLikes` is public. #### List liked packages -Displays list of of packages you have liked. Returns list of `packageLikes` +Returns the packages liked by the authenticated user. ```dart -final likes = await client.listPackageLikes(); +final likes = await client.listPackageLikes(); ``` #### Package Like Status @@ -144,107 +214,119 @@ final likes = await client.listPackageLikes(); Returns like status of a package. ```dart -final like = await client.likePackageStatus(); +final like = await client.likePackageStatus('pkg_name'); ``` #### Like a Package -Likes a package. The method returns `packageLike` payload +Likes a package and returns its authenticated like status. ```dart -final like = await client.likePackage('pkg_name'); +final like = await client.likePackage('pkg_name'); ``` #### Unlike a Package -Unlikes a package. The method returns `packageLike` payload +Unlikes a package. ```dart -final like = await client.unlikePackage('pkg_name'); +await client.unlikePackage('pkg_name'); ``` ### Search Packages -Search for packages on pub.dev. Will return the packages that match the query. You can filter the search with tags. +Searches pub.dev for packages matching a query, with optional tag and topic +filters. ```dart -final results = await client.search( +final results = await client.search( 'query', tags: [ PackageTag.publisher('publisher_id'), PackageTag.dependency('dependency_name'), 'another:tag', ], - topics:['topic_1', 'topic_2'], + topics: ['topic_1', 'topic_2'], ); // Returns the packages that match the query -print(results.packages) +print(results.packages); ``` #### Sorting search results -You are able to sort search results by the following: +Search results support these orderings: -##### Top +| Value | Orders by | +|---|---| +| `SearchOrder.top` | Weighted text, downloads, points, and likes | +| `SearchOrder.text` | Text-match similarity | +| `SearchOrder.created` | Package creation time | +| `SearchOrder.updated` | Last update time | +| `SearchOrder.downloads` | Download count | +| `SearchOrder.like` | Like count | +| `SearchOrder.points` | Pub points | +| `SearchOrder.trending` | Trend score | -Search score should be a weighted value of [text], [popularity], [points] and [like], ordered decreasing. - -##### Text +```dart +final results = await client.search('query', sort: SearchOrder.updated); -Search score should depend only on text match similarity, ordered decreasing. +print(results.packages); +``` -##### Created +> `SearchOrder.popularity` is deprecated. pub.dev no longer accepts it and +> silently serves those requests as `SearchOrder.top`. Use +> `SearchOrder.downloads`. -Search order should be in decreasing last package creation time. +#### Paging Search Results -##### Updated +Use the `next` URL to page through search results. -Search order should be in decreasing last package updated time. +```dart +final results = await client.search('query'); +final nextPage = results.next; +if (nextPage != null) { + final nextResults = await client.nextPage(nextPage); + print(nextResults.packages); +} +``` -##### Popularity +To retrieve a specific result page, pass the `page` parameter directly. -Search order should be in decreasing popularity score. +```dart +final results = await client.search('query', page: 2); +print(results.packages); +``` -##### Like +> pub.dev stops returning a `next` link after page 10, so paging through a +> search yields at most 100 packages. -Search order should be in decreasing like count. +### Completion Data -##### Points +#### Package Names -Search order should be in decreasing pub points. +`packageNameCompletion` returns the top package names on pub.dev, and +`packageNames` returns every package name in one gzip-compressed response. ```dart -final results = await client.search('query', sort: SearchOrder.updated); - -print(results.packages) +final top = await client.packageNameCompletion(); +final all = await client.packageNames(); ``` -#### Paging Search Results +#### Topics -You are able to page search results. +`topicNameCompletion` returns every topic mapped to the number of packages +using it. ```dart -final results = await client.search('query'); -final nextPage = results.next; -if (nextPage != null) { - final nextResults = await client.nextPage(nextPage); -} - -print(nextResults.packages) -``` - -If you want to retrieve a specific result page you can call the `page` parameter directly. - -```dart -final results = await client.search('query',page:2); -print(results.packages) +final topics = await client.topicNameCompletion(); +// {'flutter': 1234, 'http': 567, ...} ``` ## Utilities ### Flutter Favorites -Returns all Flutter favorites on pub.dev +Returns all Flutter favorites on pub.dev. ```dart final results = await client.fetchFlutterFavorites(); @@ -252,7 +334,7 @@ final results = await client.fetchFlutterFavorites(); ### Google Packages -Returns all official Google packages. This will be a large payload with hundreds of packages. +Returns official Google packages across the known Google publishers. ```dart final results = await client.fetchGooglePackages(); @@ -260,17 +342,24 @@ final results = await client.fetchGooglePackages(); ### Publisher Packages -Returns all packages for a specific publisher +Returns all packages for a specific publisher. ```dart - -final results = await client.fetchPublisherPackages(); +final results = await client.fetchPublisherPackages('dart.dev'); ``` ### All Packages -Returns all packages that match a given query +Returns all packages that match a given query. ```dart -final results = await fetchAllPackages('', tags: [PackageTag.publisher('leoafarias.com')]) +final results = await client.fetchAllPackages( + '', + tags: [PackageTag.publisher('leoafarias.com')], +); ``` + +> pub.dev caps each search at 10 pages. `fetchAllPackages`, +> `fetchPublisherPackages`, and `fetchFlutterFavorites` therefore return at +> most 100 packages per query. `fetchGooglePackages` applies the same limit +> separately to each known publisher. diff --git a/analysis_options.yaml b/analysis_options.yaml index 72e9538..c3c98ca 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -11,6 +11,8 @@ analyzer: exclude: - "**/*.g.dart" - "**/*.freezed.dart" + # dart_mappable output; regenerated by build_runner, never hand-edited. + - "**/*.mapper.dart" language: strict-inference: true @@ -25,8 +27,6 @@ linter: - avoid_private_typedef_functions - avoid_redundant_argument_values - avoid_renaming_method_parameters - - avoid_returning_null - - avoid_returning_null_for_future - avoid_returning_null_for_void - avoid_returning_this - avoid_single_cascade_in_expression_statements @@ -52,7 +52,6 @@ linter: - non_constant_identifier_names - only_throw_errors - overridden_fields - - package_api_docs - package_names - package_prefixed_library_names - prefer_asserts_in_initializer_lists diff --git a/lib/src/endpoints.dart b/lib/src/endpoints.dart index 0364255..dddf110 100644 --- a/lib/src/endpoints.dart +++ b/lib/src/endpoints.dart @@ -8,6 +8,7 @@ class Endpoint { late final String apiUrl; late final String searchUrl; late final String packageUrl; + late final String publisherUrl; late final String accountUrl; /// Constructor for API endpoints based on an [url] @@ -15,6 +16,7 @@ class Endpoint { apiUrl = '$baseUrl/api'; searchUrl = '$apiUrl/search'; packageUrl = '$apiUrl/packages'; + publisherUrl = '$apiUrl/publishers'; accountUrl = '$apiUrl/account'; } @@ -33,6 +35,9 @@ class Endpoint { /// Package publisher endpoint String packagePublisher(String name) => '$packageUrl/$name/publisher'; + /// The public package-like count endpoint. + String packageLikes(String name) => '$packageUrl/$name/likes'; + /// Package documentation endpoint String packageDocumentation(String name) => '$apiUrl/documentation/$name'; @@ -47,12 +52,26 @@ class Endpoint { String packageVersionInfo(String name, String version) => '$packageUrl/$name/versions/$version'; + /// The score endpoint for one package version. + String packageVersionScore(String name, String version) => + '${packageVersionInfo(name, version)}/score'; + + /// The options endpoint for one package version. + String packageVersionOptions(String name, String version) => + '${packageVersionInfo(name, version)}/options'; + + /// The public publisher-profile endpoint. + String publisherInfo(String publisherId) => '$publisherUrl/$publisherId'; + /// Retrieve all package names on pub.dev String get packageNames => '$apiUrl/package-names'; /// Package names for name completion String get packageNameCompletion => '$apiUrl/package-name-completion-data'; + /// Topic names, with the number of packages using each topic. + String get topicNameCompletion => '$apiUrl/topic-name-completion-data'; + /// Url to add and remove likes String likePackage(String name) => '$accountUrl/likes/$name'; @@ -60,12 +79,20 @@ class Endpoint { String get likedPackages => '$accountUrl/likes'; /// Search endpoint + /// + /// [query] carries the free text along with any `tag:value` filters, so it + /// is percent-encoded here. Interpolating it raw would let characters like + /// `#` and `&` terminate the query string. String search( String query, int page, SearchOrder sort, ) => - '$searchUrl?q=$query&page=$page&sort=${sort.name}'; + Uri.parse(searchUrl).replace(queryParameters: { + 'q': query, + 'page': '$page', + 'sort': sort.name, + }).toString(); /// Next search page String nextPage(String nextPageUrl) => diff --git a/lib/src/models/barrel.dart b/lib/src/models/barrel.dart index cf152f9..ef1882e 100644 --- a/lib/src/models/barrel.dart +++ b/lib/src/models/barrel.dart @@ -2,11 +2,14 @@ export 'package:pub_api_client/src/models/latest_version_model.dart'; export 'package:pub_api_client/src/models/package_advisories_model.dart'; export 'package:pub_api_client/src/models/package_documentation_model.dart'; export 'package:pub_api_client/src/models/package_like_model.dart'; +export 'package:pub_api_client/src/models/package_likes_model.dart'; export 'package:pub_api_client/src/models/package_metrics_model.dart'; export 'package:pub_api_client/src/models/package_options_model.dart'; export 'package:pub_api_client/src/models/package_publisher_model.dart'; export 'package:pub_api_client/src/models/package_score_card.dart'; export 'package:pub_api_client/src/models/package_score_model.dart'; +export 'package:pub_api_client/src/models/package_version_options_model.dart'; export 'package:pub_api_client/src/models/pub_package_model.dart'; +export 'package:pub_api_client/src/models/publisher_info_model.dart'; export 'package:pub_api_client/src/models/search_order.dart'; export 'package:pub_api_client/src/models/search_results_model.dart'; diff --git a/lib/src/models/latest_version_model.dart b/lib/src/models/latest_version_model.dart index ba70bef..b48e688 100644 --- a/lib/src/models/latest_version_model.dart +++ b/lib/src/models/latest_version_model.dart @@ -4,20 +4,35 @@ import '../../pub_api_client.dart'; part 'latest_version_model.mapper.dart'; -/// Latest Version update for package +/// Describes whether a newer package version exists. +/// +/// No pub.dev endpoint returns this shape and no client method produces it; +/// it is unreachable API surface kept only for source compatibility. +/// Compare [PubPackage.version] against the installed version instead. +@Deprecated( + 'Use PubPackage.version instead. Scheduled for removal in 5.0.0.', +) @MappableClass() class LatestVersion with LatestVersionMappable { + /// Whether [latestVersion] is newer than the installed version. final bool needUpdate; + + /// The latest published version string. final String latestVersion; + + /// Information about the latest published package version. final PubPackage packageInfo; - /// Constructor + /// Creates a latest-version result. LatestVersion({ required this.needUpdate, required this.latestVersion, required this.packageInfo, }); + /// Decodes a latest-version result from a map. static const fromMap = LatestVersionMapper.fromMap; + + /// Decodes a latest-version result from JSON. static const fromJson = LatestVersionMapper.fromJson; } diff --git a/lib/src/models/latest_version_model.mapper.dart b/lib/src/models/latest_version_model.mapper.dart index 546eef4..557c243 100644 --- a/lib/src/models/latest_version_model.mapper.dart +++ b/lib/src/models/latest_version_model.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/lib/src/models/package_advisories_model.mapper.dart b/lib/src/models/package_advisories_model.mapper.dart index 741fad7..b71bc85 100644 --- a/lib/src/models/package_advisories_model.mapper.dart +++ b/lib/src/models/package_advisories_model.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter @@ -70,14 +71,17 @@ mixin PackageAdvisoriesMappable { .encodeMap(this as PackageAdvisories); } - PackageAdvisoriesCopyWith - get copyWith => - _PackageAdvisoriesCopyWithImpl( - this as PackageAdvisories, - $identity, - $identity, - ); + PackageAdvisoriesCopyWith< + PackageAdvisories, + PackageAdvisories, + PackageAdvisories + > + get copyWith => + _PackageAdvisoriesCopyWithImpl( + this as PackageAdvisories, + $identity, + $identity, + ); @override String toString() { return PackageAdvisoriesMapper.ensureInitialized().stringifyValue( @@ -104,16 +108,23 @@ mixin PackageAdvisoriesMappable { extension PackageAdvisoriesValueCopy<$R, $Out> on ObjectCopyWith<$R, PackageAdvisories, $Out> { PackageAdvisoriesCopyWith<$R, PackageAdvisories, $Out> - get $asPackageAdvisories => $base.as( - (v, t, t2) => _PackageAdvisoriesCopyWithImpl<$R, $Out>(v, t, t2), - ); + get $asPackageAdvisories => $base.as( + (v, t, t2) => _PackageAdvisoriesCopyWithImpl<$R, $Out>(v, t, t2), + ); } -abstract class PackageAdvisoriesCopyWith<$R, $In extends PackageAdvisories, - $Out> implements ClassCopyWith<$R, $In, $Out> { - ListCopyWith<$R, SecurityAdvisory, - SecurityAdvisoryCopyWith<$R, SecurityAdvisory, SecurityAdvisory>> - get advisories; +abstract class PackageAdvisoriesCopyWith< + $R, + $In extends PackageAdvisories, + $Out +> + implements ClassCopyWith<$R, $In, $Out> { + ListCopyWith< + $R, + SecurityAdvisory, + SecurityAdvisoryCopyWith<$R, SecurityAdvisory, SecurityAdvisory> + > + get advisories; $R call({List? advisories, DateTime? advisoriesUpdated}); PackageAdvisoriesCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( Then<$Out2, $R2> t, @@ -129,38 +140,39 @@ class _PackageAdvisoriesCopyWithImpl<$R, $Out> late final ClassMapperBase $mapper = PackageAdvisoriesMapper.ensureInitialized(); @override - ListCopyWith<$R, SecurityAdvisory, - SecurityAdvisoryCopyWith<$R, SecurityAdvisory, SecurityAdvisory>> - get advisories => ListCopyWith( - $value.advisories, - (v, t) => v.copyWith.$chain(t), - (v) => call(advisories: v), - ); + ListCopyWith< + $R, + SecurityAdvisory, + SecurityAdvisoryCopyWith<$R, SecurityAdvisory, SecurityAdvisory> + > + get advisories => ListCopyWith( + $value.advisories, + (v, t) => v.copyWith.$chain(t), + (v) => call(advisories: v), + ); @override $R call({ List? advisories, Object? advisoriesUpdated = $none, - }) => - $apply( - FieldCopyWithData({ - if (advisories != null) #advisories: advisories, - if (advisoriesUpdated != $none) #advisoriesUpdated: advisoriesUpdated, - }), - ); + }) => $apply( + FieldCopyWithData({ + if (advisories != null) #advisories: advisories, + if (advisoriesUpdated != $none) #advisoriesUpdated: advisoriesUpdated, + }), + ); @override PackageAdvisories $make(CopyWithData data) => PackageAdvisories( - advisories: data.get(#advisories, or: $value.advisories), - advisoriesUpdated: data.get( - #advisoriesUpdated, - or: $value.advisoriesUpdated, - ), - ); + advisories: data.get(#advisories, or: $value.advisories), + advisoriesUpdated: data.get( + #advisoriesUpdated, + or: $value.advisoriesUpdated, + ), + ); @override PackageAdvisoriesCopyWith<$R2, PackageAdvisories, $Out2> $chain<$R2, $Out2>( Then<$Out2, $R2> t, - ) => - _PackageAdvisoriesCopyWithImpl<$R2, $Out2>($value, $cast, t); + ) => _PackageAdvisoriesCopyWithImpl<$R2, $Out2>($value, $cast, t); } class SecurityAdvisoryMapper extends ClassMapperBase { @@ -201,7 +213,7 @@ class SecurityAdvisoryMapper extends ClassMapperBase { static Map? _$databaseSpecific(SecurityAdvisory v) => v.databaseSpecific; static const Field> - _f$databaseSpecific = Field( + _f$databaseSpecific = Field( 'databaseSpecific', _$databaseSpecific, key: r'database_specific', @@ -251,12 +263,12 @@ mixin SecurityAdvisoryMappable { } SecurityAdvisoryCopyWith - get copyWith => - _SecurityAdvisoryCopyWithImpl( - this as SecurityAdvisory, - $identity, - $identity, - ); + get copyWith => + _SecurityAdvisoryCopyWithImpl( + this as SecurityAdvisory, + $identity, + $identity, + ); @override String toString() { return SecurityAdvisoryMapper.ensureInitialized().stringifyValue( @@ -283,16 +295,16 @@ mixin SecurityAdvisoryMappable { extension SecurityAdvisoryValueCopy<$R, $Out> on ObjectCopyWith<$R, SecurityAdvisory, $Out> { SecurityAdvisoryCopyWith<$R, SecurityAdvisory, $Out> - get $asSecurityAdvisory => $base - .as((v, t, t2) => _SecurityAdvisoryCopyWithImpl<$R, $Out>(v, t, t2)); + get $asSecurityAdvisory => + $base.as((v, t, t2) => _SecurityAdvisoryCopyWithImpl<$R, $Out>(v, t, t2)); } abstract class SecurityAdvisoryCopyWith<$R, $In extends SecurityAdvisory, $Out> implements ClassCopyWith<$R, $In, $Out> { ListCopyWith<$R, Affected, AffectedCopyWith<$R, Affected, Affected>>? - get affected; - MapCopyWith<$R, String, dynamic, ObjectCopyWith<$R, dynamic, dynamic>>? - get databaseSpecific; + get affected; + MapCopyWith<$R, String, dynamic, ObjectCopyWith<$R, dynamic, dynamic>?>? + get databaseSpecific; $R call({ String? id, String? summary, @@ -315,22 +327,22 @@ class _SecurityAdvisoryCopyWithImpl<$R, $Out> SecurityAdvisoryMapper.ensureInitialized(); @override ListCopyWith<$R, Affected, AffectedCopyWith<$R, Affected, Affected>>? - get affected => $value.affected != null - ? ListCopyWith( - $value.affected!, - (v, t) => v.copyWith.$chain(t), - (v) => call(affected: v), - ) - : null; - @override - MapCopyWith<$R, String, dynamic, ObjectCopyWith<$R, dynamic, dynamic>>? - get databaseSpecific => $value.databaseSpecific != null - ? MapCopyWith( - $value.databaseSpecific!, - (v, t) => ObjectCopyWith(v, $identity, t), - (v) => call(databaseSpecific: v), - ) - : null; + get affected => $value.affected != null + ? ListCopyWith( + $value.affected!, + (v, t) => v.copyWith.$chain(t), + (v) => call(affected: v), + ) + : null; + @override + MapCopyWith<$R, String, dynamic, ObjectCopyWith<$R, dynamic, dynamic>?>? + get databaseSpecific => $value.databaseSpecific != null + ? MapCopyWith( + $value.databaseSpecific!, + (v, t) => ObjectCopyWith(v, $identity, t), + (v) => call(databaseSpecific: v), + ) + : null; @override $R call({ String? id, @@ -338,31 +350,28 @@ class _SecurityAdvisoryCopyWithImpl<$R, $Out> Object? details = $none, Object? affected = $none, Object? databaseSpecific = $none, - }) => - $apply( - FieldCopyWithData({ - if (id != null) #id: id, - if (summary != $none) #summary: summary, - if (details != $none) #details: details, - if (affected != $none) #affected: affected, - if (databaseSpecific != $none) #databaseSpecific: databaseSpecific, - }), - ); + }) => $apply( + FieldCopyWithData({ + if (id != null) #id: id, + if (summary != $none) #summary: summary, + if (details != $none) #details: details, + if (affected != $none) #affected: affected, + if (databaseSpecific != $none) #databaseSpecific: databaseSpecific, + }), + ); @override SecurityAdvisory $make(CopyWithData data) => SecurityAdvisory( - id: data.get(#id, or: $value.id), - summary: data.get(#summary, or: $value.summary), - details: data.get(#details, or: $value.details), - affected: data.get(#affected, or: $value.affected), - databaseSpecific: - data.get(#databaseSpecific, or: $value.databaseSpecific), - ); + id: data.get(#id, or: $value.id), + summary: data.get(#summary, or: $value.summary), + details: data.get(#details, or: $value.details), + affected: data.get(#affected, or: $value.affected), + databaseSpecific: data.get(#databaseSpecific, or: $value.databaseSpecific), + ); @override SecurityAdvisoryCopyWith<$R2, SecurityAdvisory, $Out2> $chain<$R2, $Out2>( Then<$Out2, $R2> t, - ) => - _SecurityAdvisoryCopyWithImpl<$R2, $Out2>($value, $cast, t); + ) => _SecurityAdvisoryCopyWithImpl<$R2, $Out2>($value, $cast, t); } class AffectedMapper extends ClassMapperBase { @@ -466,12 +475,12 @@ class _AffectedCopyWithImpl<$R, $Out> @override ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>>? get versions => $value.versions != null - ? ListCopyWith( - $value.versions!, - (v, t) => ObjectCopyWith(v, $identity, t), - (v) => call(versions: v), - ) - : null; + ? ListCopyWith( + $value.versions!, + (v, t) => ObjectCopyWith(v, $identity, t), + (v) => call(versions: v), + ) + : null; @override $R call({Object? versions = $none}) => $apply(FieldCopyWithData({if (versions != $none) #versions: versions})); @@ -482,6 +491,6 @@ class _AffectedCopyWithImpl<$R, $Out> @override AffectedCopyWith<$R2, Affected, $Out2> $chain<$R2, $Out2>( Then<$Out2, $R2> t, - ) => - _AffectedCopyWithImpl<$R2, $Out2>($value, $cast, t); + ) => _AffectedCopyWithImpl<$R2, $Out2>($value, $cast, t); } + diff --git a/lib/src/models/package_documentation_model.mapper.dart b/lib/src/models/package_documentation_model.mapper.dart index 9da7db1..ace0c76 100644 --- a/lib/src/models/package_documentation_model.mapper.dart +++ b/lib/src/models/package_documentation_model.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/lib/src/models/package_like_model.mapper.dart b/lib/src/models/package_like_model.mapper.dart index 8487d65..7b2b9f4 100644 --- a/lib/src/models/package_like_model.mapper.dart +++ b/lib/src/models/package_like_model.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/lib/src/models/package_likes_model.dart b/lib/src/models/package_likes_model.dart new file mode 100644 index 0000000..c47d8a8 --- /dev/null +++ b/lib/src/models/package_likes_model.dart @@ -0,0 +1,27 @@ +import 'package:dart_mappable/dart_mappable.dart'; + +part 'package_likes_model.mapper.dart'; + +/// Public like count of a package. +/// +/// Unlike the authenticated like-status API, this information is public. +@MappableClass() +class PackageLikes with PackageLikesMappable { + /// The package whose likes were counted. + final String package; + + /// The package's public like count. + final int likes; + + /// Creates a package-like count. + const PackageLikes({ + required this.package, + required this.likes, + }); + + /// Decodes a package-like count from a map. + static const fromMap = PackageLikesMapper.fromMap; + + /// Decodes a package-like count from JSON. + static const fromJson = PackageLikesMapper.fromJson; +} diff --git a/lib/src/models/package_likes_model.mapper.dart b/lib/src/models/package_likes_model.mapper.dart new file mode 100644 index 0000000..abf1bd4 --- /dev/null +++ b/lib/src/models/package_likes_model.mapper.dart @@ -0,0 +1,138 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format off +// ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member +// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member +// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter + +part of 'package_likes_model.dart'; + +class PackageLikesMapper extends ClassMapperBase { + PackageLikesMapper._(); + + static PackageLikesMapper? _instance; + static PackageLikesMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = PackageLikesMapper._()); + } + return _instance!; + } + + @override + final String id = 'PackageLikes'; + + static String _$package(PackageLikes v) => v.package; + static const Field _f$package = Field( + 'package', + _$package, + ); + static int _$likes(PackageLikes v) => v.likes; + static const Field _f$likes = Field('likes', _$likes); + + @override + final MappableFields fields = const { + #package: _f$package, + #likes: _f$likes, + }; + + static PackageLikes _instantiate(DecodingData data) { + return PackageLikes( + package: data.dec(_f$package), + likes: data.dec(_f$likes), + ); + } + + @override + final Function instantiate = _instantiate; + + static PackageLikes fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static PackageLikes fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin PackageLikesMappable { + String toJson() { + return PackageLikesMapper.ensureInitialized().encodeJson( + this as PackageLikes, + ); + } + + Map toMap() { + return PackageLikesMapper.ensureInitialized().encodeMap( + this as PackageLikes, + ); + } + + PackageLikesCopyWith get copyWith => + _PackageLikesCopyWithImpl( + this as PackageLikes, + $identity, + $identity, + ); + @override + String toString() { + return PackageLikesMapper.ensureInitialized().stringifyValue( + this as PackageLikes, + ); + } + + @override + bool operator ==(Object other) { + return PackageLikesMapper.ensureInitialized().equalsValue( + this as PackageLikes, + other, + ); + } + + @override + int get hashCode { + return PackageLikesMapper.ensureInitialized().hashValue( + this as PackageLikes, + ); + } +} + +extension PackageLikesValueCopy<$R, $Out> + on ObjectCopyWith<$R, PackageLikes, $Out> { + PackageLikesCopyWith<$R, PackageLikes, $Out> get $asPackageLikes => + $base.as((v, t, t2) => _PackageLikesCopyWithImpl<$R, $Out>(v, t, t2)); +} + +abstract class PackageLikesCopyWith<$R, $In extends PackageLikes, $Out> + implements ClassCopyWith<$R, $In, $Out> { + $R call({String? package, int? likes}); + PackageLikesCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); +} + +class _PackageLikesCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, PackageLikes, $Out> + implements PackageLikesCopyWith<$R, PackageLikes, $Out> { + _PackageLikesCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + PackageLikesMapper.ensureInitialized(); + @override + $R call({String? package, int? likes}) => $apply( + FieldCopyWithData({ + if (package != null) #package: package, + if (likes != null) #likes: likes, + }), + ); + @override + PackageLikes $make(CopyWithData data) => PackageLikes( + package: data.get(#package, or: $value.package), + likes: data.get(#likes, or: $value.likes), + ); + + @override + PackageLikesCopyWith<$R2, PackageLikes, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t, + ) => _PackageLikesCopyWithImpl<$R2, $Out2>($value, $cast, t); +} + diff --git a/lib/src/models/package_metrics_model.mapper.dart b/lib/src/models/package_metrics_model.mapper.dart index a1b489b..ce11073 100644 --- a/lib/src/models/package_metrics_model.mapper.dart +++ b/lib/src/models/package_metrics_model.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/lib/src/models/package_options_model.mapper.dart b/lib/src/models/package_options_model.mapper.dart index 0074f49..031e280 100644 --- a/lib/src/models/package_options_model.mapper.dart +++ b/lib/src/models/package_options_model.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/lib/src/models/package_publisher_model.mapper.dart b/lib/src/models/package_publisher_model.mapper.dart index 29cf9e7..70ffe41 100644 --- a/lib/src/models/package_publisher_model.mapper.dart +++ b/lib/src/models/package_publisher_model.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/lib/src/models/package_score_card.dart b/lib/src/models/package_score_card.dart index d3f1139..c00aefc 100644 --- a/lib/src/models/package_score_card.dart +++ b/lib/src/models/package_score_card.dart @@ -241,4 +241,7 @@ class Repository with RepositoryMappable { required this.repository, required this.branch, }); + + static const fromMap = RepositoryMapper.fromMap; + static const fromJson = RepositoryMapper.fromJson; } diff --git a/lib/src/models/package_score_model.dart b/lib/src/models/package_score_model.dart index de15a64..cbf9378 100644 --- a/lib/src/models/package_score_model.dart +++ b/lib/src/models/package_score_model.dart @@ -2,15 +2,32 @@ import 'package:dart_mappable/dart_mappable.dart'; part 'package_score_model.mapper.dart'; +/// Score data for a package version. +/// +/// Returned by both package-score and package-version-score requests. @MappableClass() class PackageScore with PackageScoreMappable { + /// The points awarded by the latest pub.dev analysis. final int? grantedPoints; + + /// The maximum points available in the latest pub.dev analysis. final int? maxPoints; + + /// The package's public like count. final int likeCount; + + /// pub.dev retired the popularity score and no longer returns this field, + /// so it is always `null`. Use [downloadCount30Days] instead. + @Deprecated('Use downloadCount30Days instead.') final double? popularityScore; + + /// The package's download count over the last 30 days. final int? downloadCount30Days; + + /// Search tags assigned to the package version by pub.dev. final List tags; + /// Creates package score data. PackageScore({ required this.grantedPoints, required this.maxPoints, @@ -20,6 +37,9 @@ class PackageScore with PackageScoreMappable { required this.tags, }); + /// Decodes package score data from a map. static const fromMap = PackageScoreMapper.fromMap; + + /// Decodes package score data from JSON. static const fromJson = PackageScoreMapper.fromJson; } diff --git a/lib/src/models/package_score_model.mapper.dart b/lib/src/models/package_score_model.mapper.dart index c1e45ab..6a27ac9 100644 --- a/lib/src/models/package_score_model.mapper.dart +++ b/lib/src/models/package_score_model.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/lib/src/models/package_version_options_model.dart b/lib/src/models/package_version_options_model.dart new file mode 100644 index 0000000..0d3bd69 --- /dev/null +++ b/lib/src/models/package_version_options_model.dart @@ -0,0 +1,19 @@ +import 'package:dart_mappable/dart_mappable.dart'; + +part 'package_version_options_model.mapper.dart'; + +/// Retraction options for a package version. +@MappableClass() +class PackageVersionOptions with PackageVersionOptionsMappable { + /// Whether pub.dev has marked the version as retracted. + final bool isRetracted; + + /// Creates package-version options. + const PackageVersionOptions({this.isRetracted = false}); + + /// Decodes package-version options from a map. + static const fromMap = PackageVersionOptionsMapper.fromMap; + + /// Decodes package-version options from JSON. + static const fromJson = PackageVersionOptionsMapper.fromJson; +} diff --git a/lib/src/models/package_version_options_model.mapper.dart b/lib/src/models/package_version_options_model.mapper.dart new file mode 100644 index 0000000..0490b5f --- /dev/null +++ b/lib/src/models/package_version_options_model.mapper.dart @@ -0,0 +1,141 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format off +// ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member +// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member +// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter + +part of 'package_version_options_model.dart'; + +class PackageVersionOptionsMapper + extends ClassMapperBase { + PackageVersionOptionsMapper._(); + + static PackageVersionOptionsMapper? _instance; + static PackageVersionOptionsMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = PackageVersionOptionsMapper._()); + } + return _instance!; + } + + @override + final String id = 'PackageVersionOptions'; + + static bool _$isRetracted(PackageVersionOptions v) => v.isRetracted; + static const Field _f$isRetracted = Field( + 'isRetracted', + _$isRetracted, + opt: true, + def: false, + ); + + @override + final MappableFields fields = const { + #isRetracted: _f$isRetracted, + }; + + static PackageVersionOptions _instantiate(DecodingData data) { + return PackageVersionOptions(isRetracted: data.dec(_f$isRetracted)); + } + + @override + final Function instantiate = _instantiate; + + static PackageVersionOptions fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static PackageVersionOptions fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin PackageVersionOptionsMappable { + String toJson() { + return PackageVersionOptionsMapper.ensureInitialized() + .encodeJson(this as PackageVersionOptions); + } + + Map toMap() { + return PackageVersionOptionsMapper.ensureInitialized() + .encodeMap(this as PackageVersionOptions); + } + + PackageVersionOptionsCopyWith< + PackageVersionOptions, + PackageVersionOptions, + PackageVersionOptions + > + get copyWith => + _PackageVersionOptionsCopyWithImpl< + PackageVersionOptions, + PackageVersionOptions + >(this as PackageVersionOptions, $identity, $identity); + @override + String toString() { + return PackageVersionOptionsMapper.ensureInitialized().stringifyValue( + this as PackageVersionOptions, + ); + } + + @override + bool operator ==(Object other) { + return PackageVersionOptionsMapper.ensureInitialized().equalsValue( + this as PackageVersionOptions, + other, + ); + } + + @override + int get hashCode { + return PackageVersionOptionsMapper.ensureInitialized().hashValue( + this as PackageVersionOptions, + ); + } +} + +extension PackageVersionOptionsValueCopy<$R, $Out> + on ObjectCopyWith<$R, PackageVersionOptions, $Out> { + PackageVersionOptionsCopyWith<$R, PackageVersionOptions, $Out> + get $asPackageVersionOptions => $base.as( + (v, t, t2) => _PackageVersionOptionsCopyWithImpl<$R, $Out>(v, t, t2), + ); +} + +abstract class PackageVersionOptionsCopyWith< + $R, + $In extends PackageVersionOptions, + $Out +> + implements ClassCopyWith<$R, $In, $Out> { + $R call({bool? isRetracted}); + PackageVersionOptionsCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t, + ); +} + +class _PackageVersionOptionsCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, PackageVersionOptions, $Out> + implements PackageVersionOptionsCopyWith<$R, PackageVersionOptions, $Out> { + _PackageVersionOptionsCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + PackageVersionOptionsMapper.ensureInitialized(); + @override + $R call({bool? isRetracted}) => $apply( + FieldCopyWithData({if (isRetracted != null) #isRetracted: isRetracted}), + ); + @override + PackageVersionOptions $make(CopyWithData data) => PackageVersionOptions( + isRetracted: data.get(#isRetracted, or: $value.isRetracted), + ); + + @override + PackageVersionOptionsCopyWith<$R2, PackageVersionOptions, $Out2> + $chain<$R2, $Out2>(Then<$Out2, $R2> t) => + _PackageVersionOptionsCopyWithImpl<$R2, $Out2>($value, $cast, t); +} + diff --git a/lib/src/models/pub_package_model.mapper.dart b/lib/src/models/pub_package_model.mapper.dart index 5a8d972..2cbd467 100644 --- a/lib/src/models/pub_package_model.mapper.dart +++ b/lib/src/models/pub_package_model.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter diff --git a/lib/src/models/publisher_info_model.dart b/lib/src/models/publisher_info_model.dart new file mode 100644 index 0000000..23d539d --- /dev/null +++ b/lib/src/models/publisher_info_model.dart @@ -0,0 +1,29 @@ +import 'package:dart_mappable/dart_mappable.dart'; + +part 'publisher_info_model.mapper.dart'; + +/// Public profile information for a publisher. +@MappableClass() +class PublisherInfo with PublisherInfoMappable { + /// The publisher's Markdown-formatted description. + final String? description; + + /// The publisher's website URL. + final String? websiteUrl; + + /// The publisher's verified contact email address. + final String? contactEmail; + + /// Creates a publisher profile. + const PublisherInfo({ + this.description, + this.websiteUrl, + this.contactEmail, + }); + + /// Decodes a publisher profile from a map. + static const fromMap = PublisherInfoMapper.fromMap; + + /// Decodes a publisher profile from JSON. + static const fromJson = PublisherInfoMapper.fromJson; +} diff --git a/lib/src/models/publisher_info_model.mapper.dart b/lib/src/models/publisher_info_model.mapper.dart new file mode 100644 index 0000000..cf09efa --- /dev/null +++ b/lib/src/models/publisher_info_model.mapper.dart @@ -0,0 +1,157 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format off +// ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member +// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member +// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter + +part of 'publisher_info_model.dart'; + +class PublisherInfoMapper extends ClassMapperBase { + PublisherInfoMapper._(); + + static PublisherInfoMapper? _instance; + static PublisherInfoMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = PublisherInfoMapper._()); + } + return _instance!; + } + + @override + final String id = 'PublisherInfo'; + + static String? _$description(PublisherInfo v) => v.description; + static const Field _f$description = Field( + 'description', + _$description, + opt: true, + ); + static String? _$websiteUrl(PublisherInfo v) => v.websiteUrl; + static const Field _f$websiteUrl = Field( + 'websiteUrl', + _$websiteUrl, + opt: true, + ); + static String? _$contactEmail(PublisherInfo v) => v.contactEmail; + static const Field _f$contactEmail = Field( + 'contactEmail', + _$contactEmail, + opt: true, + ); + + @override + final MappableFields fields = const { + #description: _f$description, + #websiteUrl: _f$websiteUrl, + #contactEmail: _f$contactEmail, + }; + + static PublisherInfo _instantiate(DecodingData data) { + return PublisherInfo( + description: data.dec(_f$description), + websiteUrl: data.dec(_f$websiteUrl), + contactEmail: data.dec(_f$contactEmail), + ); + } + + @override + final Function instantiate = _instantiate; + + static PublisherInfo fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static PublisherInfo fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin PublisherInfoMappable { + String toJson() { + return PublisherInfoMapper.ensureInitialized().encodeJson( + this as PublisherInfo, + ); + } + + Map toMap() { + return PublisherInfoMapper.ensureInitialized().encodeMap( + this as PublisherInfo, + ); + } + + PublisherInfoCopyWith + get copyWith => _PublisherInfoCopyWithImpl( + this as PublisherInfo, + $identity, + $identity, + ); + @override + String toString() { + return PublisherInfoMapper.ensureInitialized().stringifyValue( + this as PublisherInfo, + ); + } + + @override + bool operator ==(Object other) { + return PublisherInfoMapper.ensureInitialized().equalsValue( + this as PublisherInfo, + other, + ); + } + + @override + int get hashCode { + return PublisherInfoMapper.ensureInitialized().hashValue( + this as PublisherInfo, + ); + } +} + +extension PublisherInfoValueCopy<$R, $Out> + on ObjectCopyWith<$R, PublisherInfo, $Out> { + PublisherInfoCopyWith<$R, PublisherInfo, $Out> get $asPublisherInfo => + $base.as((v, t, t2) => _PublisherInfoCopyWithImpl<$R, $Out>(v, t, t2)); +} + +abstract class PublisherInfoCopyWith<$R, $In extends PublisherInfo, $Out> + implements ClassCopyWith<$R, $In, $Out> { + $R call({String? description, String? websiteUrl, String? contactEmail}); + PublisherInfoCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); +} + +class _PublisherInfoCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, PublisherInfo, $Out> + implements PublisherInfoCopyWith<$R, PublisherInfo, $Out> { + _PublisherInfoCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + PublisherInfoMapper.ensureInitialized(); + @override + $R call({ + Object? description = $none, + Object? websiteUrl = $none, + Object? contactEmail = $none, + }) => $apply( + FieldCopyWithData({ + if (description != $none) #description: description, + if (websiteUrl != $none) #websiteUrl: websiteUrl, + if (contactEmail != $none) #contactEmail: contactEmail, + }), + ); + @override + PublisherInfo $make(CopyWithData data) => PublisherInfo( + description: data.get(#description, or: $value.description), + websiteUrl: data.get(#websiteUrl, or: $value.websiteUrl), + contactEmail: data.get(#contactEmail, or: $value.contactEmail), + ); + + @override + PublisherInfoCopyWith<$R2, PublisherInfo, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t, + ) => _PublisherInfoCopyWithImpl<$R2, $Out2>($value, $cast, t); +} + diff --git a/lib/src/models/search_order.dart b/lib/src/models/search_order.dart index c01d821..6d4cd63 100644 --- a/lib/src/models/search_order.dart +++ b/lib/src/models/search_order.dart @@ -2,10 +2,15 @@ import 'package:dart_mappable/dart_mappable.dart'; part 'search_order.mapper.dart'; +/// Mirrors the `SearchOrder` enum pub.dev accepts on `sort=`. +/// +/// pub.dev silently falls back to [SearchOrder.top] for values it does not +/// recognize, so unknown orders fail quietly rather than erroring. @MappableEnum() enum SearchOrder { - /// Search score should be a weighted value of [text], [popularity], [points] - /// and [like], ordered decreasing. + /// Search score should be a weighted value of [SearchOrder.text], + /// [SearchOrder.downloads], [SearchOrder.points], and [SearchOrder.like], + /// ordered decreasing. top, /// Search score should depend only on text match similarity, ordered @@ -19,6 +24,11 @@ enum SearchOrder { updated, /// Search order should be in decreasing popularity score. + /// + /// pub.dev retired the popularity score and no longer accepts this value; + /// requests using it are served as [SearchOrder.top]. Use + /// [SearchOrder.downloads] instead. + @Deprecated('Use SearchOrder.downloads instead.') popularity, /// Search order should be in decreasing download count. @@ -28,9 +38,13 @@ enum SearchOrder { like, /// Search order should be in decreasing pub points. - points; + points, + + /// Search order should be in decreasing trend score. + trending; const SearchOrder(); + /// The value sent in pub.dev's `sort` query parameter. String get value => name; } diff --git a/lib/src/models/search_order.mapper.dart b/lib/src/models/search_order.mapper.dart index c2350fb..f45f048 100644 --- a/lib/src/models/search_order.mapper.dart +++ b/lib/src/models/search_order.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter @@ -42,6 +43,8 @@ class SearchOrderMapper extends EnumMapper { return SearchOrder.like; case r'points': return SearchOrder.points; + case r'trending': + return SearchOrder.trending; default: throw MapperException.unknownEnumValue(value); } @@ -66,6 +69,8 @@ class SearchOrderMapper extends EnumMapper { return r'like'; case SearchOrder.points: return r'points'; + case SearchOrder.trending: + return r'trending'; } } } diff --git a/lib/src/models/search_results_model.dart b/lib/src/models/search_results_model.dart index e2c6385..36ce517 100644 --- a/lib/src/models/search_results_model.dart +++ b/lib/src/models/search_results_model.dart @@ -48,9 +48,14 @@ class SearchResults extends PaginatedResults @override final String? next; + /// Set when pub.dev could not fully honour the query, e.g. when a tag in + /// the query is unknown. Results are still returned alongside it. + final String? message; + const SearchResults({ required this.packages, this.next, + this.message, }); static const fromMap = SearchResultsMapper.fromMap; diff --git a/lib/src/models/search_results_model.mapper.dart b/lib/src/models/search_results_model.mapper.dart index 431ec6f..3af28d6 100644 --- a/lib/src/models/search_results_model.mapper.dart +++ b/lib/src/models/search_results_model.mapper.dart @@ -2,6 +2,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // dart format off // ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter @@ -183,17 +184,25 @@ class SearchResultsMapper extends ClassMapperBase { _$next, opt: true, ); + static String? _$message(SearchResults v) => v.message; + static const Field _f$message = Field( + 'message', + _$message, + opt: true, + ); @override final MappableFields fields = const { #packages: _f$packages, #next: _f$next, + #message: _f$message, }; static SearchResults _instantiate(DecodingData data) { return SearchResults( packages: data.dec(_f$packages), next: data.dec(_f$next), + message: data.dec(_f$message), ); } @@ -265,7 +274,7 @@ abstract class SearchResultsCopyWith<$R, $In extends SearchResults, $Out> PackageResultCopyWith<$R, PackageResult, PackageResult> > get packages; - $R call({List? packages, String? next}); + $R call({List? packages, String? next, String? message}); SearchResultsCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); } @@ -289,16 +298,22 @@ class _SearchResultsCopyWithImpl<$R, $Out> (v) => call(packages: v), ); @override - $R call({List? packages, Object? next = $none}) => $apply( + $R call({ + List? packages, + Object? next = $none, + Object? message = $none, + }) => $apply( FieldCopyWithData({ if (packages != null) #packages: packages, if (next != $none) #next: next, + if (message != $none) #message: message, }), ); @override SearchResults $make(CopyWithData data) => SearchResults( packages: data.get(#packages, or: $value.packages), next: data.get(#next, or: $value.next), + message: data.get(#message, or: $value.message), ); @override diff --git a/lib/src/pub_api_client_base.dart b/lib/src/pub_api_client_base.dart index ef531f5..72fce12 100644 --- a/lib/src/pub_api_client_base.dart +++ b/lib/src/pub_api_client_base.dart @@ -11,11 +11,14 @@ import 'helpers/recursive_paging.dart'; import 'models/package_advisories_model.dart'; import 'models/package_documentation_model.dart'; import 'models/package_like_model.dart'; +import 'models/package_likes_model.dart'; import 'models/package_metrics_model.dart'; import 'models/package_options_model.dart'; import 'models/package_publisher_model.dart'; import 'models/package_score_model.dart'; +import 'models/package_version_options_model.dart'; import 'models/pub_package_model.dart'; +import 'models/publisher_info_model.dart'; import 'models/search_order.dart'; import 'models/search_results_model.dart'; import 'version.dart'; @@ -35,12 +38,21 @@ String _buildUserAgentHeader(String userAgentMetadata) => /// Pub API Client class PubClient { + /// The resolved pub.dev endpoint URLs used by this client. final Endpoint endpoint; + + /// The custom pub server URL, or `null` when using pub.dev. final String? pubUrl; + + /// Whether to log request URLs through `dart:developer`. final bool debug; late http.Client _client; late Map _headers = {}; + /// Creates a client for pub.dev or the custom [pubUrl]. + /// + /// Supplying [credentials] enables authenticated endpoints. An injected + /// [client] is used for every request and is closed by [close]. PubClient({ this.pubUrl, Credentials? credentials, @@ -68,13 +80,33 @@ class PubClient { _client = client ?? httpClient; } - Future> _fetch(String url) async { + /// Merges [overrides] over the default headers. + /// + /// HTTP header names are case-insensitive, so a plain map spread would send + /// both `Accept` and `accept` rather than replacing one with the other. + Map _headersWith(Map? overrides) { + if (overrides == null || overrides.isEmpty) { + return _headers; + } + final overridden = overrides.keys.map((key) => key.toLowerCase()).toSet(); + return { + for (final entry in _headers.entries) + if (!overridden.contains(entry.key.toLowerCase())) + entry.key: entry.value, + ...overrides, + }; + } + + Future> _fetch( + String url, { + Map? headers, + }) async { if (debug) { log('Fetching: $url'); } final response = await _client.get( Uri.parse(url), - headers: _headers, + headers: _headersWith(headers), ); responseValidOrThrow(response); @@ -137,6 +169,20 @@ class PubClient { return PackagePublisher.fromMap(data); } + /// Returns the public like count for package [packageName]. + /// + /// Unlike [likePackageStatus] this does not require authentication. + Future packageLikes(String packageName) async { + final data = await _fetch(endpoint.packageLikes(packageName)); + return PackageLikes.fromMap(data); + } + + /// Returns public profile information for [publisherId]. + Future publisherInfo(String publisherId) async { + final data = await _fetch(endpoint.publisherInfo(publisherId)); + return PublisherInfo.fromMap(data); + } + /// Returns a list of versions that are published for package [packageName] Future> packageVersions(String packageName) async { final data = await _fetch(endpoint.packageVersions(packageName)); @@ -157,7 +203,29 @@ class PubClient { return PackageVersion.fromMap(data); } + /// Returns the score for [version] of [packageName]. + Future packageVersionScore( + String packageName, String version) async { + final data = await _fetch( + endpoint.packageVersionScore(packageName, version), + ); + return PackageScore.fromMap(data); + } + + /// Returns whether [version] of [packageName] is retracted. + Future packageVersionOptions( + String packageName, String version) async { + final data = await _fetch( + endpoint.packageVersionOptions(packageName, version), + ); + return PackageVersionOptions.fromMap(data); + } + /// Returns a `List` of all packages listed on pub.dev + /// + /// pub.dev answers 406 unless the request accepts gzip. `package:http` + /// negotiates and decodes that transparently, so an injected [http.Client] + /// must not disable it. Future> packageNames() async { final data = await _fetch(endpoint.packageNames); final results = PackageNamesResults.fromMap(data); @@ -177,6 +245,21 @@ class PubClient { return packages.cast(); } + /// Returns every topic on pub.dev mapped to the number of packages + /// using it, ordered by pub.dev's own ranking. + /// + /// Like [packageNames] this endpoint requires a gzip-accepting request; + /// pub.dev returns gzipped bytes regardless of `Accept-Encoding`. + Future> topicNameCompletion() async { + // This endpoint rejects the `application/vnd.pub.v2+json` Accept header + // that every other endpoint requires. + final data = await _fetch( + endpoint.topicNameCompletion, + headers: {'Accept': 'application/json'}, + ); + return data.map((topic, count) => MapEntry(topic, (count as num).toInt())); + } + /// Searches pub for [query] and can [page] results. /// Can specify [tags] to filter results. See [PackageTag] for details. /// returns `SearchResults` @@ -251,6 +334,9 @@ class PubClient { } /// Get all packages that match the query + /// + /// pub.dev stops returning a `next` link after page 10, so this is + /// capped at 100 results regardless of how many packages match. Future> fetchAllPackages( String query, { List tags = const [], @@ -263,6 +349,9 @@ class PubClient { /// Mostly used as an internal tool to generate /// google_packages_list.dart /// You should probably use that instead + /// + /// Each publisher is paged through [fetchAllPackages], so this returns at + /// most 100 packages per publisher. Future> fetchGooglePackages({ List tags = const [], }) async { @@ -273,7 +362,8 @@ class PubClient { 'material.io', 'firebase.google.com', 'google.dev', - 'tools.dart.dev ', + 'tools.dart.dev', + 'labs.dart.dev', ]; final futures = >>[]; @@ -292,6 +382,7 @@ class PubClient { return results.map((r) => r.package).toList(); } + /// Returns packages published by [publisherName], with optional [tags]. Future> fetchPublisherPackages( String publisherName, { List tags = const [], @@ -301,6 +392,7 @@ class PubClient { ...tags, ]); + /// Closes the underlying HTTP client. void close() { _client.close(); } diff --git a/lib/src/version.dart b/lib/src/version.dart index 518a182..66803ec 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -1,2 +1,2 @@ // Generated code. Do not modify. -const packageVersion = '3.2.0'; +const packageVersion = '4.0.0'; diff --git a/pubspec.yaml b/pubspec.yaml index befd268..e2b94e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: pub_api_client description: An API Client for Pub to interact with public package information. -version: 3.2.0 +version: 4.0.0 homepage: https://github.com/leoafarias/pub_api_client environment: diff --git a/test/advisories_integration_test.dart b/test/advisories_integration_test.dart index 850ffc2..e26d1db 100644 --- a/test/advisories_integration_test.dart +++ b/test/advisories_integration_test.dart @@ -3,7 +3,8 @@ import 'package:test/test.dart'; import 'test_utils.dart'; -/// Integration test for security advisories using packages known to have advisories +/// Integration test for security advisories using packages known to have +/// advisories void main() { final client = PubClient( debug: true, @@ -28,7 +29,8 @@ void main() { expect(firstAdvisory.pubDisplayUrl, isNotNull); expect(firstAdvisory.pubDisplayUrl, contains('github.com/advisories/')); - print('\nFound ${advisories.advisories.length} advisory(ies) for http package'); + print('\nFound ${advisories.advisories.length} advisory(ies) ' + 'for http package'); print('Advisory ID: ${firstAdvisory.id}'); print('Summary: ${firstAdvisory.summary}'); print('URL: ${firstAdvisory.pubDisplayUrl}'); @@ -44,7 +46,8 @@ void main() { expect(advisories.advisoriesUpdated, isNotNull); expect(advisories.advisories, isNotEmpty); - print('\nFound ${advisories.advisories.length} advisory(ies) for dio package'); + print('\nFound ${advisories.advisories.length} advisory(ies) ' + 'for dio package'); for (final advisory in advisories.advisories) { print('- ${advisory.id}: ${advisory.summary}'); } diff --git a/test/endpoints_test.dart b/test/endpoints_test.dart index fda5393..ed9cceb 100644 --- a/test/endpoints_test.dart +++ b/test/endpoints_test.dart @@ -44,6 +44,16 @@ void main() { equals('${Endpoint.defaultBaseUrl}/api/packages/bloc/publisher')); }); + test('packageLikes returns correct URL', () { + expect(endpoint.packageLikes('bloc'), + equals('${Endpoint.defaultBaseUrl}/api/packages/bloc/likes')); + }); + + test('publisherInfo returns correct URL', () { + expect(endpoint.publisherInfo('dart.dev'), + equals('${Endpoint.defaultBaseUrl}/api/publishers/dart.dev')); + }); + test('packageDocumentation returns correct URL', () { expect(endpoint.packageDocumentation('provider'), equals('${Endpoint.defaultBaseUrl}/api/documentation/provider')); @@ -66,11 +76,30 @@ void main() { '${Endpoint.defaultBaseUrl}/api/packages/get_it/versions/1.0.0')); }); + test('packageVersionScore returns correct URL', () { + expect( + endpoint.packageVersionScore('get_it', '1.0.0'), + equals('${Endpoint.defaultBaseUrl}' + '/api/packages/get_it/versions/1.0.0/score')); + }); + + test('packageVersionOptions returns correct URL', () { + expect( + endpoint.packageVersionOptions('get_it', '1.0.0'), + equals('${Endpoint.defaultBaseUrl}' + '/api/packages/get_it/versions/1.0.0/options')); + }); + test('packageNames returns correct URL', () { expect(endpoint.packageNames, equals('${Endpoint.defaultBaseUrl}/api/package-names')); }); + test('topicNameCompletion returns correct URL', () { + expect(endpoint.topicNameCompletion, + equals('${Endpoint.defaultBaseUrl}/api/topic-name-completion-data')); + }); + test('likePackage returns correct URL', () { expect(endpoint.likePackage('dio'), equals('${Endpoint.defaultBaseUrl}/api/account/likes/dio')); @@ -88,6 +117,17 @@ void main() { '${Endpoint.defaultBaseUrl}/api/search?q=flutter&page=1&sort=top')); }); + test('search percent-encodes the query', () { + final url = endpoint.search( + 'c# & c++ publisher:dart.dev', 2, SearchOrder.downloads); + + expect(Uri.parse(url).queryParameters, { + 'q': 'c# & c++ publisher:dart.dev', + 'page': '2', + 'sort': 'downloads', + }); + }); + test('nextPage returns correct URL with custom base URL', () { final customEndpoint = Endpoint('https://custom.pub.dev'); expect( diff --git a/test/pubdev_api_test.dart b/test/pubdev_api_test.dart index ceaebf8..3692ce2 100644 --- a/test/pubdev_api_test.dart +++ b/test/pubdev_api_test.dart @@ -176,6 +176,57 @@ void main() { expect(unregisterPublisher.publisherId, null); }); + test('Get publisher info', () async { + final publisher = await _client.publisherInfo('dart.dev'); + + expect(publisher.description, isNotEmpty); + expect(publisher.websiteUrl, isNotEmpty); + expect(publisher.contactEmail, isNotEmpty); + }); + + test('Get publisher info for unknown publisher throws', () async { + expect( + () => _client.publisherInfo('does-not-exist-xyz.dev'), + throwsA(isA()), + ); + }); + + test('Get package likes', () async { + final likes = await _client.packageLikes(packageName); + + expect(likes.package, packageName); + expect(likes.likes, greaterThanOrEqualTo(0)); + }); + + test('Get package version score', () async { + final package = await _client.packageInfo(packageName); + final score = await _client.packageVersionScore( + packageName, + package.version, + ); + + expect(score.likeCount, greaterThan(50)); + expect(score.tags, isNotEmpty); + }); + + test('Get package version options', () async { + final package = await _client.packageInfo(packageName); + final options = await _client.packageVersionOptions( + packageName, + package.version, + ); + + expect(options.isRetracted, package.latest.retracted); + }); + + test('Get package version options for a retracted version', () async { + // sqlite3 2.4.1 is retracted on pub.dev; without a known-retracted + // version the test above only ever exercises `isRetracted: false`. + final options = await _client.packageVersionOptions('sqlite3', '2.4.1'); + + expect(options.isRetracted, isTrue); + }); + test('Get Package Score', () async { final payload = await _client.packageScore(packageName); @@ -200,33 +251,42 @@ void main() { test('Sort search results for packages', () async { final updated = await _client.search('', sort: SearchOrder.updated); - final popularity = await _client.search('', sort: SearchOrder.popularity); final downloads = await _client.search('', sort: SearchOrder.downloads); final points = await _client.search('', sort: SearchOrder.points); final created = await _client.search('', sort: SearchOrder.created); final text = await _client.search('', sort: SearchOrder.text); + final trending = await _client.search('', sort: SearchOrder.trending); final top = await _client.search(''); final updatedCopy = await _client.search('', sort: SearchOrder.updated); - expect(updated, isNot(popularity)); - expect(popularity, isNot(points)); - expect(downloads, isNot(popularity)); + expect(updated, isNot(downloads)); + expect(downloads, isNot(points)); expect(points, isNot(updated)); expect(updated, isNot(created)); expect(created, isNot(text)); expect(text, isNot(top)); expect(top, isNot(updated)); + expect(trending, isNot(top)); expect(updated, updatedCopy); + }); - expect(SearchOrder.points.value, 'points'); - expect(SearchOrder.popularity.value, 'popularity'); - expect(SearchOrder.downloads.value, 'downloads'); - expect(SearchOrder.created.value, 'created'); - expect(SearchOrder.text.value, 'text'); - expect(SearchOrder.top.value, 'top'); - expect(SearchOrder.like.value, 'like'); + test('Search order serializes to the pub.dev `sort` parameter', () { + expect( + SearchOrder.values.map((order) => order.value), + [ + 'top', + 'text', + 'created', + 'updated', + 'popularity', + 'downloads', + 'like', + 'points', + 'trending', + ], + ); }); test('Search for packages of a publisher', () async { @@ -267,6 +327,23 @@ void main() { expect(zeroResults.packages.length, 0); }); + test('Get topic name completion', () async { + final topics = await _client.topicNameCompletion(); + + expect(topics.length, greaterThan(100)); + expect(topics['flutter'], greaterThan(0)); + }); + + test('Search encodes reserved characters in the query', () async { + // An unencoded `#` opens a URI fragment, silently truncating the query. + // Both searches would collapse to `q=flutter` and return identical + // results if the query were not percent-encoded. + final plain = await _client.search('flutter'); + final withHash = await _client.search('flutter#zzzz'); + + expect(withHash.packages, isNot(plain.packages)); + }); + test('Exceptions', () async { void mockRes(int code) { final res = Response('{error:{message:"test"}}', code); @@ -291,12 +368,10 @@ void main() { }); test('Get package security advisories', () async { - // Test with actual package - may return null if endpoint not supported - // Note: This endpoint is not yet implemented on pub.dev as of the test date - // but the client correctly handles the 404 by returning null + // Servers that do not implement the endpoint answer 404, which the + // client maps to null. final advisories = await _client.packageAdvisories(packageName); - // The endpoint should either return null (not implemented) or a valid response if (advisories != null) { // If the endpoint is implemented, verify the structure expect(advisories.advisories, isA>()); diff --git a/test/request_headers_test.dart b/test/request_headers_test.dart new file mode 100644 index 0000000..db32803 --- /dev/null +++ b/test/request_headers_test.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:pub_api_client/pub_api_client.dart'; +import 'package:test/test.dart'; + +void main() { + group('Request headers', () { + test('sends the pub v2 Accept header by default', () async { + Map? captured; + final client = PubClient( + client: MockClient((request) async { + captured = request.headers; + return http.Response( + jsonEncode({'packages': []}), + 200, + headers: {'content-type': 'application/json'}, + ); + }), + ); + + await client.packageNameCompletion(); + + expect(captured?['accept'], 'application/vnd.pub.v2+json'); + }); + + test( + 'topic completion replaces the Accept header instead of adding ' + 'a second one', () async { + // pub.dev answers 406 for `application/vnd.pub.v2+json` on this + // endpoint, so the override has to replace it, not append to it. + Map? captured; + final client = PubClient( + client: MockClient((request) async { + captured = request.headers; + return http.Response( + jsonEncode({'flutter': 1}), + 200, + headers: {'content-type': 'application/json'}, + ); + }), + ); + + await client.topicNameCompletion(); + + expect(captured?['accept'], 'application/json'); + expect( + captured!.keys.where((key) => key.toLowerCase() == 'accept'), + hasLength(1), + ); + // The default headers survive the override. + expect(captured?['user-agent'], startsWith('pub_api_client/')); + }); + }); +} diff --git a/test/test_utils.dart b/test/test_utils.dart index a5b4420..64dfce9 100644 --- a/test/test_utils.dart +++ b/test/test_utils.dart @@ -154,7 +154,7 @@ class RateLimitedClient extends http.BaseClient { if (_lastRequestTime != null) { final timeSinceLastRequest = DateTime.now().difference(_lastRequestTime!); if (timeSinceLastRequest < _minDelay) { - await Future.delayed(_minDelay - timeSinceLastRequest); + await Future.delayed(_minDelay - timeSinceLastRequest); } } _lastRequestTime = DateTime.now();