diff --git a/fly.toml b/fly.toml index 30c10b0baa603..e8f27f5e1c67d 100644 --- a/fly.toml +++ b/fly.toml @@ -45,3 +45,7 @@ processes = [] interval = "15s" restart_limit = 0 timeout = "2s" + +[[vm]] + size = "shared-cpu-1x" + memory = "256mb" diff --git a/services/archlinux/archlinux.tester.js b/services/archlinux/archlinux.tester.js index 122868ce07f3d..4ad56d5ec9293 100644 --- a/services/archlinux/archlinux.tester.js +++ b/services/archlinux/archlinux.tester.js @@ -3,7 +3,7 @@ import { createServiceTester } from '../tester.js' export const t = await createServiceTester() t.create('Arch Linux package (valid)') - .get('/core/x86_64/pacman.json') + .get('/core/x86_64/iptables.json') .expectBadge({ label: 'arch linux', message: isVPlusDottedVersionNClausesWithOptionalSuffixAndEpoch, diff --git a/services/aur/aur.service.js b/services/aur/aur.service.js index 3eb8e68753db9..ee8381f7245c1 100644 --- a/services/aur/aur.service.js +++ b/services/aur/aur.service.js @@ -4,7 +4,7 @@ import { age as ageColor, } from '../color-formatters.js' import { renderLicenseBadge } from '../licenses.js' -import { addv, metric, formatDate } from '../text-formatters.js' +import { metric, formatDate } from '../text-formatters.js' import { nonNegativeInteger } from '../validators.js' import { BaseJsonService, @@ -12,6 +12,7 @@ import { InvalidResponse, pathParams, } from '../index.js' +import { renderVersionBadge } from '../version.js' const aurSchema = Joi.object({ resultcount: nonNegativeInteger, @@ -170,7 +171,7 @@ class AurVersion extends BaseAurService { static render({ version, outOfDate }) { const color = outOfDate === null ? 'blue' : 'orange' - return { message: addv(version), color } + return renderVersionBadge({ version, versionFormatter: () => color }) } async handle({ packageName }) { diff --git a/services/aur/aur.spec.js b/services/aur/aur.spec.js index 0bdc44bec721b..a707fa7d5f1c2 100644 --- a/services/aur/aur.spec.js +++ b/services/aur/aur.spec.js @@ -6,11 +6,13 @@ describe('AurVersion', function () { given({ version: '1:1.1.42.622-1', outOfDate: 1 }).expect({ color: 'orange', message: 'v1:1.1.42.622-1', + label: undefined, }) given({ version: '7', outOfDate: null }).expect({ color: 'blue', message: 'v7', + label: undefined, }) }) }) diff --git a/services/conda/conda-version.service.js b/services/conda/conda-version.service.js index ab9d57c08577c..fa915f94b7610 100644 --- a/services/conda/conda-version.service.js +++ b/services/conda/conda-version.service.js @@ -1,6 +1,5 @@ import { pathParams } from '../index.js' -import { addv as versionText } from '../text-formatters.js' -import { version as versionColor } from '../color-formatters.js' +import { renderVersionBadge } from '../version.js' import BaseCondaService from './conda-base.js' export default class CondaVersion extends BaseCondaService { @@ -33,20 +32,12 @@ export default class CondaVersion extends BaseCondaService { }, } - static render({ variant, channel, version }) { - return { - label: variant === 'vn' ? channel : `conda | ${channel}`, - message: versionText(version), - color: versionColor(version), - } - } - async handle({ variant, channel, packageName }) { const json = await this.fetch({ channel, packageName }) - return this.constructor.render({ - variant, - channel, + const defaultLabel = variant === 'vn' ? channel : `conda | ${channel}` + return renderVersionBadge({ version: json.latest_version, + defaultLabel, }) } } 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' }) diff --git a/services/hackage/hackage-deps.service.js b/services/hackage/hackage-deps.service.js index bec34ddd45b7a..07e3b01360240 100644 --- a/services/hackage/hackage-deps.service.js +++ b/services/hackage/hackage-deps.service.js @@ -1,48 +1,11 @@ -import { BaseService, pathParams } from '../index.js' +import { deprecatedService } from '../index.js' -export default class HackageDeps extends BaseService { - static category = 'dependencies' - - static route = { +export const HackageDeps = deprecatedService({ + category: 'dependencies', + route: { base: 'hackage-deps/v', pattern: ':packageName', - } - - static openApi = { - '/hackage-deps/v/{packageName}': { - get: { - summary: 'Hackage Dependencies', - parameters: pathParams({ - name: 'packageName', - example: 'lens', - }), - }, - }, - } - - static defaultBadgeData = { label: 'dependencies' } - - static render({ isOutdated }) { - if (isOutdated) { - return { message: 'outdated', color: 'orange' } - } else { - return { message: 'up to date', color: 'brightgreen' } - } - } - - async handle({ packageName }) { - const reverseUrl = `http://packdeps.haskellers.com/licenses/${packageName}` - const feedUrl = `http://packdeps.haskellers.com/feed/${packageName}` - - // first call /reverse to check if the package exists - // this will throw a 404 if it doesn't - await this._request({ url: reverseUrl }) - - // if the package exists, then query /feed to check the dependencies - const { buffer } = await this._request({ url: feedUrl }) - - const outdatedStr = `Outdated dependencies for ${packageName} ` - const isOutdated = buffer.includes(outdatedStr) - return this.constructor.render({ isOutdated }) - } -} + }, + label: 'hackagedeps', + dateAdded: new Date('2024-10-18'), +}) diff --git a/services/hackage/hackage-deps.tester.js b/services/hackage/hackage-deps.tester.js index 6133b776e6099..aa9e065e8cb3d 100644 --- a/services/hackage/hackage-deps.tester.js +++ b/services/hackage/hackage-deps.tester.js @@ -1,14 +1,10 @@ -import Joi from 'joi' -import { createServiceTester } from '../tester.js' -export const t = await createServiceTester() +import { ServiceTester } from '../tester.js' +export const t = new ServiceTester({ + id: 'hackagedeps', + title: 'Hackage Dependencies', + pathPrefix: '/hackage-deps/v', +}) -t.create('hackage deps (valid)') - .get('/lens.json') - .expectBadge({ - label: 'dependencies', - message: Joi.string().regex(/^(up to date|outdated)$/), - }) - -t.create('hackage deps (not found)') - .get('/not-a-package.json') - .expectBadge({ label: 'dependencies', message: 'not found' }) +t.create('hackage deps (deprecated)') + .get('/package.json') + .expectBadge({ label: 'hackagedeps', message: 'no longer available' }) diff --git a/services/npm/npm-base.spec.js b/services/npm/npm-base.spec.js index 4ec6301601b7e..b8a6bec47f007 100644 --- a/services/npm/npm-base.spec.js +++ b/services/npm/npm-base.spec.js @@ -34,7 +34,7 @@ describe('npm', function () { await NpmVersion.invoke(defaultContext, config, { packageName: 'npm' }), ).to.deep.equal({ color: 'orange', - label: undefined, + label: 'npm', message: 'v0.1.0', }) diff --git a/services/version.js b/services/version.js index 7322ceab5c0e7..cdbb90aec3b30 100644 --- a/services/version.js +++ b/services/version.js @@ -234,6 +234,8 @@ function rangeStart(v) { * @param {string} options.version - The version number to display on the badge * @param {string} [options.tag] - The tag to display on the badge, such as "alpha" or "beta" * @param {string} [options.defaultLabel] - The default label to display on the badge, such as "npm" or "github" + * @param {string} [options.prefix] - The prefix to display on the message, such as ">=", "v", overrides the default behavior of using addv + * @param {string} [options.postfix] - The postfix to display on the message, such as "tested" * @param {Function} [options.versionFormatter=versionColor] - The function to use to format the color of the badge based on the version number * @returns {object} A badge object that has three properties: label, message, and color * @example @@ -245,11 +247,15 @@ function renderVersionBadge({ version, tag, defaultLabel, + prefix, + postfix, versionFormatter = versionColor, }) { return { - label: tag ? `${defaultLabel}@${tag}` : undefined, - message: addv(version), + label: tag ? `${defaultLabel}@${tag}` : defaultLabel, + message: + (prefix ? `${prefix}${version}` : addv(version)) + + (postfix ? ` ${postfix}` : ''), color: versionFormatter(version), } } diff --git a/services/version.spec.js b/services/version.spec.js index d4ecce758d221..251cfc892a6be 100644 --- a/services/version.spec.js +++ b/services/version.spec.js @@ -150,5 +150,69 @@ describe('Version helpers', function () { message: 'v1.2.3', color: 'blue', }) + given({ version: '1.2.3', defaultLabel: 'npm' }).expect({ + label: 'npm', + message: 'v1.2.3', + color: 'blue', + }) + given({ version: '1.2.3', postfix: 'tested' }).expect({ + label: undefined, + message: 'v1.2.3 tested', + color: 'blue', + }) + given({ + version: '1.2.3', + tag: 'beta', + defaultLabel: 'github', + postfix: 'tested', + }).expect({ + label: 'github@beta', + message: 'v1.2.3 tested', + color: 'blue', + }) + given({ version: '1.2.3', prefix: '^' }).expect({ + label: undefined, + message: '^1.2.3', + color: 'blue', + }) + given({ + version: '1.2.3', + tag: 'alpha', + defaultLabel: 'npm', + prefix: '^', + }).expect({ + label: 'npm@alpha', + message: '^1.2.3', + color: 'blue', + }) + given({ + version: '1.2.3', + defaultLabel: 'npm', + prefix: '^', + }).expect({ + label: 'npm', + message: '^1.2.3', + color: 'blue', + }) + given({ + version: '1.2.3', + prefix: '^', + postfix: 'tested', + }).expect({ + label: undefined, + message: '^1.2.3 tested', + color: 'blue', + }) + given({ + version: '1.2.3', + tag: 'beta', + defaultLabel: 'github', + prefix: '^', + postfix: 'tested', + }).expect({ + label: 'github@beta', + message: '^1.2.3 tested', + color: 'blue', + }) }) }) diff --git a/services/wordpress/wordpress-platform.service.js b/services/wordpress/wordpress-platform.service.js index 974409aebe058..4de1087ff68a3 100644 --- a/services/wordpress/wordpress-platform.service.js +++ b/services/wordpress/wordpress-platform.service.js @@ -1,6 +1,5 @@ import { NotFound, pathParams } from '../index.js' -import { addv } from '../text-formatters.js' -import { version as versionColor } from '../color-formatters.js' +import { renderVersionBadge } from '../version.js' import { description, BaseWordpress } from './wordpress-base.js' import { versionColorForWordpressVersion } from './wordpress-version-color.js' @@ -46,13 +45,6 @@ function WordpressRequiresVersion(extensionType) { static defaultBadgeData = { label: 'wordpress' } - static render({ wordpressVersion }) { - return { - message: addv(wordpressVersion), - color: versionColor(wordpressVersion), - } - } - async handle({ slug }) { const { requires: wordpressVersion } = await this.fetch({ extensionType, @@ -65,7 +57,7 @@ function WordpressRequiresVersion(extensionType) { }) } - return this.constructor.render({ wordpressVersion }) + return renderVersionBadge({ version: wordpressVersion }) } } } @@ -93,21 +85,18 @@ class WordpressPluginTestedVersion extends BaseWordpress { static defaultBadgeData = { label: 'wordpress' } - static async render({ testedVersion }) { - // Atypically, the `render()` function of this badge is `async` because it needs to pull - // data from the server. - return { - message: `${addv(testedVersion)} tested`, - color: await versionColorForWordpressVersion(testedVersion), - } - } - async handle({ slug }) { const { tested: testedVersion } = await this.fetch({ extensionType: 'plugin', slug, }) - return this.constructor.render({ testedVersion }) + // Atypically, pulling color data from the server with async operation. + const color = await versionColorForWordpressVersion(testedVersion) + return renderVersionBadge({ + version: testedVersion, + postfix: 'tested', + versionFormatter: () => color, + }) } } @@ -142,14 +131,6 @@ function RequiresPHPVersionForType(extensionType) { static defaultBadgeData = { label: 'php' } - static render({ version }) { - return { - label: 'php', - message: `>=${version}`, - color: versionColor(version), - } - } - async handle({ slug }) { const { requires_php: requiresPhp } = await this.fetch({ extensionType, @@ -162,9 +143,7 @@ function RequiresPHPVersionForType(extensionType) { }) } - return this.constructor.render({ - version: requiresPhp, - }) + return renderVersionBadge({ version: requiresPhp, prefix: '>=' }) } } }