From ca22d01606f889747d76be7f3a51c05d5e446fd4 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Mon, 22 Apr 2019 22:55:31 -0400 Subject: [PATCH] Rewrite [GithubDownloads] (#3351) For consistency with other download badges, I changed some formatting: - **downloads | 24k total** -> **downloads | 24k** - **downloads | 3k** -> **downloads@latest | 3k** - **downloads | 3k v0.29.0** -> **downloads@v0.29.0 | 3k** Ref #2863 --- services/github/github-downloads.service.js | 280 ++++++++++---------- services/github/github-downloads.tester.js | 50 ++-- 2 files changed, 159 insertions(+), 171 deletions(-) diff --git a/services/github/github-downloads.service.js b/services/github/github-downloads.service.js index 532469fb0b..fae0a44e16 100644 --- a/services/github/github-downloads.service.js +++ b/services/github/github-downloads.service.js @@ -1,21 +1,30 @@ 'use strict' -const LegacyService = require('../legacy-service') -const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') -const { makeLogo: getLogo } = require('../../lib/logos') +const Joi = require('joi') +const { NotFound } = require('..') const { metric } = require('../text-formatters') -const { - documentation, - checkErrorResponse: githubCheckErrorResponse, -} = require('./github-helpers') +const { nonNegativeInteger } = require('../validators') +const { downloadCount: downloadCountColor } = require('../color-formatters') +const { GithubAuthService } = require('./github-auth-service') +const { documentation, errorMessagesFor } = require('./github-helpers') -// This legacy service should be rewritten to use e.g. BaseJsonService. -// -// Tips for rewriting: -// https://github.com/badges/shields/blob/master/doc/rewriting-services.md -// -// Do not base new services on this code. -module.exports = class GithubDownloads extends LegacyService { +const releaseSchema = Joi.object({ + assets: Joi.array() + .items({ + name: Joi.string().required(), + download_count: nonNegativeInteger, + }) + .required(), +}).required() + +const releaseArraySchema = Joi.alternatives().try( + Joi.array().items(releaseSchema), + Joi.array().length(0) +) + +const keywords = ['github download'] + +module.exports = class GithubDownloads extends GithubAuthService { static get category() { return 'downloads' } @@ -23,7 +32,7 @@ module.exports = class GithubDownloads extends LegacyService { static get route() { return { base: 'github', - pattern: '', + pattern: ':kind(downloads|downloads-pre)/:user/:repo/:tag*/:assetName', } } @@ -36,12 +45,12 @@ module.exports = class GithubDownloads extends LegacyService { user: 'atom', repo: 'atom', }, - staticPreview: { - label: 'downloads', - message: '857k total', - color: 'brightgreen', - }, + staticPreview: this.render({ + assetName: 'total', + downloadCount: 857000, + }), documentation, + keywords, }, { title: 'GitHub Releases', @@ -51,12 +60,13 @@ module.exports = class GithubDownloads extends LegacyService { repo: 'atom', tag: 'latest', }, - staticPreview: { - label: 'downloads', - message: '27k', - color: 'brightgreen', - }, + staticPreview: this.render({ + tag: 'latest', + assetName: 'total', + downloadCount: 27000, + }), documentation, + keywords, }, { title: 'GitHub Pre-Releases', @@ -66,12 +76,13 @@ module.exports = class GithubDownloads extends LegacyService { repo: 'atom', tag: 'latest', }, - staticPreview: { - label: 'downloads', - message: '2k', - color: 'brightgreen', - }, + staticPreview: this.render({ + tag: 'latest', + assetName: 'total', + downloadCount: 2000, + }), documentation, + keywords, }, { title: 'GitHub Releases (by Release)', @@ -81,12 +92,13 @@ module.exports = class GithubDownloads extends LegacyService { repo: 'atom', tag: 'v0.190.0', }, - staticPreview: { - label: 'downloads', - message: '490k v0.190.0', - color: 'brightgreen', - }, + staticPreview: this.render({ + tag: 'v0.190.0', + assetName: 'total', + downloadCount: 490000, + }), documentation, + keywords, }, { title: 'GitHub Releases (by Asset)', @@ -97,12 +109,13 @@ module.exports = class GithubDownloads extends LegacyService { tag: 'latest', path: 'atom-amd64.deb', }, - staticPreview: { - label: 'downloads', - message: '3k [atom-amd64.deb]', - color: 'brightgreen', - }, + staticPreview: this.render({ + tag: 'latest', + assetName: 'atom-amd64.deb', + downloadCount: 3000, + }), documentation, + keywords, }, { title: 'GitHub Pre-Releases (by Asset)', @@ -113,119 +126,96 @@ module.exports = class GithubDownloads extends LegacyService { tag: 'latest', path: 'atom-amd64.deb', }, - staticPreview: { - label: 'downloads', - message: '237 [atom-amd64.deb]', - color: 'brightgreen', - }, + staticPreview: this.render({ + tag: 'latest', + assetName: 'atom-amd64.deb', + downloadCount: 237, + }), documentation, + keywords, }, ] } - static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { - camp.route( - /^\/github\/(downloads|downloads-pre)\/([^/]+)\/([^/]+)(\/.+)?\/([^/]+)\.(svg|png|gif|jpg|json)$/, - cache((data, match, sendBadge, request) => { - const type = match[1] // downloads or downloads-pre - const user = match[2] // eg, qubyte/rubidium - const repo = match[3] + static get defaultBadgeData() { + return { + label: 'downloads', + namedLogo: 'github', + } + } - let tag = match[4] // eg, v0.190.0, latest, null if querying all releases - const assetName = match[5].toLowerCase() // eg. total, atom-amd64.deb, atom.x86_64.rpm - const format = match[6] + static render({ tag, assetName, downloadCount }) { + return { + label: tag ? `downloads@${tag}` : 'downloads', + message: + assetName === 'total' + ? metric(downloadCount) + : `${metric(downloadCount)} [${assetName}]`, + color: downloadCountColor(downloadCount), + } + } - if (tag) { - tag = tag.slice(1) - } + static transform({ releases, assetName }) { + const downloadCount = releases.reduce((accum1, { assets }) => { + const filteredAssets = + assetName === 'total' + ? assets + : assets.filter(({ name }) => name.toLowerCase() === assetName) + return ( + accum1 + + filteredAssets.reduce( + (accum2, { download_count: downloadCount }) => accum2 + downloadCount, + 0 + ) + ) + }, 0) + return { downloadCount } + } - let total = true - if (tag) { - total = false - } - - let apiUrl = `/repos/${user}/${repo}/releases` - if (!total) { - const releasePath = - tag === 'latest' - ? type === 'downloads' - ? 'latest' - : '' - : `tags/${tag}` - if (releasePath) { - apiUrl = `${apiUrl}/${releasePath}` - } - } else { - apiUrl = `${apiUrl}?per_page=500` - } - - const badgeData = getBadgeData('downloads', data) - if (badgeData.template === 'social') { - badgeData.logo = getLogo('github', data) - } - githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { - if ( - githubCheckErrorResponse( - badgeData, - err, - res, - 'repo or release not found' - ) - ) { - sendBadge(format, badgeData) - return - } - try { - let data = JSON.parse(buffer) - if (type === 'downloads-pre' && tag === 'latest') { - data = data[0] - } - let downloads = 0 - - const labelWords = [] - if (total) { - data.forEach(tagData => { - tagData.assets.forEach(asset => { - if ( - assetName === 'total' || - assetName === asset.name.toLowerCase() - ) { - downloads += asset.download_count - } - }) - }) - - labelWords.push('total') - if (assetName !== 'total') { - labelWords.push(`[${assetName}]`) - } - } else { - data.assets.forEach(asset => { - if ( - assetName === 'total' || - assetName === asset.name.toLowerCase() - ) { - downloads += asset.download_count - } - }) - - if (tag !== 'latest') { - labelWords.push(tag) - } - if (assetName !== 'total') { - labelWords.push(`[${assetName}]`) - } - } - labelWords.unshift(metric(downloads)) - badgeData.text[1] = labelWords.join(' ') - badgeData.colorscheme = 'brightgreen' - sendBadge(format, badgeData) - } catch (e) { - badgeData.text[1] = 'none' - sendBadge(format, badgeData) - } - }) + async handle({ kind, user, repo, tag, assetName }) { + let releases + if (tag === 'latest' && kind === 'downloads') { + const latestRelease = await this._requestJson({ + schema: releaseSchema, + url: `/repos/${user}/${repo}/releases/latest`, + errorMessages: errorMessagesFor('repo not found'), }) - ) + releases = [latestRelease] + } else if (tag === 'latest') { + // Keep only the latest release. + const [latestReleaseIncludingPrereleases] = await this._requestJson({ + schema: releaseArraySchema, + url: `/repos/${user}/${repo}/releases`, + options: { qs: { per_page: 1 } }, + errorMessages: errorMessagesFor('repo not found'), + }) + releases = [latestReleaseIncludingPrereleases] + } else if (tag) { + const wantedRelease = await this._requestJson({ + schema: releaseSchema, + url: `/repos/${user}/${repo}/releases/tags/${tag}`, + errorMessages: errorMessagesFor('repo or release not found'), + }) + releases = [wantedRelease] + } else { + const allReleases = await this._requestJson({ + schema: releaseArraySchema, + url: `/repos/${user}/${repo}/releases`, + options: { qs: { per_page: 500 } }, + errorMessages: errorMessagesFor('repo not found'), + }) + releases = allReleases + } + + if (releases.length === 0) { + throw new NotFound({ prettyMessage: 'no releases' }) + } + + const { downloadCount } = this.constructor.transform({ + releases, + assetName, + }) + + return this.constructor.render({ tag, assetName, downloadCount }) } } diff --git a/services/github/github-downloads.tester.js b/services/github/github-downloads.tester.js index 64ea5ca00b..c48af01754 100644 --- a/services/github/github-downloads.tester.js +++ b/services/github/github-downloads.tester.js @@ -6,69 +6,67 @@ const t = (module.exports = require('../tester').createServiceTester()) t.create('Downloads all releases') .get('/downloads/photonstorm/phaser/total.json') - .expectBadge({ - label: 'downloads', - message: Joi.string().regex(/^\w+\s+total$/), - }) + .expectBadge({ label: 'downloads', message: isMetric }) + +t.create('Downloads all releases (no releases)') + .get('/downloads/badges/shields/total.json') + .expectBadge({ label: 'downloads', message: 'no releases' }) + +t.create('Downloads-pre all releases (no releases)') + .get('/downloads-pre/badges/shields/total.json') + .expectBadge({ label: 'downloads', message: 'no releases' }) t.create('Downloads all releases (repo not found)') .get('/downloads/badges/helmets/total.json') - .expectBadge({ - label: 'downloads', - message: 'repo or release not found', - }) + .expectBadge({ label: 'downloads', message: 'repo not found' }) + +t.create('Downloads-pre all releases (repo not found)') + .get('/downloads-pre/badges/helmets/total.json') + .expectBadge({ label: 'downloads', message: 'repo not found' }) t.create('downloads for latest release') .get('/downloads/photonstorm/phaser/latest/total.json') - .expectBadge({ label: 'downloads', message: isMetric }) + .expectBadge({ label: 'downloads@latest', message: isMetric }) t.create('downloads-pre for latest release') .get('/downloads-pre/photonstorm/phaser/latest/total.json') - .expectBadge({ label: 'downloads', message: isMetric }) + .expectBadge({ label: 'downloads@latest', message: isMetric }) t.create('downloads for release without slash') .get('/downloads/atom/atom/v0.190.0/total.json') - .expectBadge({ - label: 'downloads', - message: Joi.string().regex(/^[0-9]+[kMGTPEZY]? v0\.190\.0$/), - }) + .expectBadge({ label: 'downloads@v0.190.0', message: isMetric }) t.create('downloads for specific asset without slash') .get('/downloads/atom/atom/v0.190.0/atom-amd64.deb.json') .expectBadge({ - label: 'downloads', - message: Joi.string().regex( - /^[0-9]+[kMGTPEZY]? v0\.190\.0 \[atom-amd64\.deb\]$/ - ), + label: 'downloads@v0.190.0', + message: Joi.string().regex(/^[0-9]+[kMGTPEZY]? \[atom-amd64\.deb\]$/), }) t.create('downloads for specific asset from latest release') .get('/downloads/atom/atom/latest/atom-amd64.deb.json') .expectBadge({ - label: 'downloads', + label: 'downloads@latest', message: Joi.string().regex(/^[0-9]+[kMGTPEZY]? \[atom-amd64\.deb\]$/), }) t.create('downloads-pre for specific asset from latest release') .get('/downloads-pre/atom/atom/latest/atom-amd64.deb.json') .expectBadge({ - label: 'downloads', + label: 'downloads@latest', message: Joi.string().regex(/^[0-9]+[kMGTPEZY]? \[atom-amd64\.deb\]$/), }) t.create('downloads for release with slash') .get('/downloads/NHellFire/dban/stable/v2.2.8/total.json') - .expectBadge({ - label: 'downloads', - message: Joi.string().regex(/^[0-9]+[kMGTPEZY]? stable\/v2\.2\.8$/), - }) + .expectBadge({ label: 'downloads@stable/v2.2.8', message: isMetric }) t.create('downloads for specific asset with slash') .get('/downloads/NHellFire/dban/stable/v2.2.8/dban-2.2.8_i586.iso.json') .expectBadge({ - label: 'downloads', + label: 'downloads@stable/v2.2.8', message: Joi.string().regex( - /^[0-9]+[kMGTPEZY]? stable\/v2\.2\.8 \[dban-2\.2\.8_i586\.iso\]$/ + /^[0-9]+[kMGTPEZY]? \[dban-2\.2\.8_i586\.iso\]$/ ), })