From 66631524c57caee15eaa4ea3e53934caf77f1342 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sun, 20 Oct 2024 23:03:54 +0300 Subject: [PATCH] Add [CratesUserDownloads] service and tester (#10619) * add jsdoc for crates fetch func * add BaseCratesUserService for user stats api route part of solution for #10614 * Add CratesUserDownloads service and tester This commit adds the CratesUserDownloads service and tester files. The CratesUserDownloads service shows the user total downloads at Crates.io. as requested by #10614 * render userid in code block * add non-exsistent user CratesUserDownloads test userid for API usage is int32, therefor to minimize chance of user taking the id used the max value for int32 is used. * fixed typo --- services/crates/crates-base.js | 30 ++++++++++++++++- .../crates/crates-user-downloads.service.js | 32 +++++++++++++++++++ .../crates/crates-user-downloads.tester.js | 16 ++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 services/crates/crates-user-downloads.service.js create mode 100644 services/crates/crates-user-downloads.tester.js diff --git a/services/crates/crates-base.js b/services/crates/crates-base.js index 7b7571c55d029..8d744ca5b6caa 100644 --- a/services/crates/crates-base.js +++ b/services/crates/crates-base.js @@ -24,9 +24,21 @@ const versionResponseSchema = Joi.object({ version: versionSchema.required(), }).required() +const userStatsSchema = Joi.object({ + total_downloads: nonNegativeInteger.required(), +}).required() + class BaseCratesService extends BaseJsonService { static defaultBadgeData = { label: 'crates.io' } + /** + * Fetches data from the crates.io API. + * + * @param {object} options - The options for the request + * @param {string} options.crate - The crate name. + * @param {string} [options.version] - The crate version number (optional). + * @returns {Promise} the JSON response from the API. + */ async fetch({ crate, version }) { const url = version ? `https://crates.io/api/v1/crates/${crate}/${version}` @@ -54,7 +66,23 @@ class BaseCratesService extends BaseJsonService { } } +class BaseCratesUserService extends BaseJsonService { + static defaultBadgeData = { label: 'crates.io' } + + /** + * Fetches data from the crates.io API. + * + * @param {object} options - The options for the request + * @param {string} options.userId - The user ID. + * @returns {Promise} the JSON response from the API. + */ + async fetch({ userId }) { + const url = `https://crates.io/api/v1/users/${userId}/stats` + return this._requestJson({ schema: userStatsSchema, url }) + } +} + const description = '[Crates.io](https://crates.io/) is a package registry for Rust.' -export { BaseCratesService, description } +export { BaseCratesService, BaseCratesUserService, description } diff --git a/services/crates/crates-user-downloads.service.js b/services/crates/crates-user-downloads.service.js new file mode 100644 index 0000000000000..e7a0fef5c2959 --- /dev/null +++ b/services/crates/crates-user-downloads.service.js @@ -0,0 +1,32 @@ +import { renderDownloadsBadge } from '../downloads.js' +import { pathParams } from '../index.js' +import { BaseCratesUserService, description } from './crates-base.js' + +export default class CratesUserDownloads extends BaseCratesUserService { + static category = 'downloads' + static route = { + base: 'crates', + pattern: 'udt/:userId', + } + + static openApi = { + '/crates/udt/{userId}': { + get: { + summary: 'Crates.io User Total Downloads', + description, + parameters: pathParams({ + name: 'userId', + example: '3027', + description: + 'The user ID can be found using `https://crates.io/api/v1/users/{username}`', + }), + }, + }, + } + + async handle({ userId }) { + const json = await this.fetch({ userId }) + const { total_downloads: downloads } = json + return renderDownloadsBadge({ downloads, labelOverride: 'downloads' }) + } +} diff --git a/services/crates/crates-user-downloads.tester.js b/services/crates/crates-user-downloads.tester.js new file mode 100644 index 0000000000000..d544a56fe35ab --- /dev/null +++ b/services/crates/crates-user-downloads.tester.js @@ -0,0 +1,16 @@ +import { isMetric } from '../test-validators.js' +import { createServiceTester } from '../tester.js' +export const t = await createServiceTester() + +t.create('total user downloads') + .get('/udt/3027.json') + .expectBadge({ label: 'downloads', message: isMetric }) + +// non-existent user returns 0 downloads with 200 OK status code rather than 404. +t.create('total user downloads (user not found)') + .get('/udt/2147483647.json') // 2147483647 is the maximum valid user id as API uses i32 + .expectBadge({ label: 'downloads', message: '0' }) + +t.create('total user downloads (invalid)') + .get('/udt/999999999999999999999999.json') + .expectBadge({ label: 'crates.io', message: 'invalid' })