From 755af712ae555af68d0b795aeb13445ef910ba32 Mon Sep 17 00:00:00 2001 From: kuanoni Date: Wed, 16 Nov 2022 12:36:19 -0800 Subject: [PATCH] [factorio-mod-portal] services (#8625) * add latest version service * add game version service * update game version service, includes version range * add game version and latest version service tests * add last updated service & tests * add downloads service * url changes * add 'range' query to factorio versions * renamed function to transform * validate downloads via nonNegativeInteger https://github.com/badges/shields/pull/8625#discussion_r1021557183 * change version route to /v https://github.com/badges/shields/pull/8625#discussion_r1021558937 * factorio-version category to platform-support https://github.com/badges/shields/pull/8625#discussion_r1021557896 * change downloads route to /dt https://github.com/badges/shields/pull/8625#discussion_r1021559841 * version validator to isVPlusDottedVersionNClauses https://github.com/badges/shields/pull/8625#discussion_r1021563736 * removed range query param from factorio-version https://github.com/badges/shields/pull/8625#discussion_r1021568758 --- .../factorio-mod-portal.service.js | 173 ++++++++++++++++++ .../factorio-mod-portal.tester.js | 47 +++++ 2 files changed, 220 insertions(+) create mode 100644 services/factorio-mod-portal/factorio-mod-portal.service.js create mode 100644 services/factorio-mod-portal/factorio-mod-portal.tester.js diff --git a/services/factorio-mod-portal/factorio-mod-portal.service.js b/services/factorio-mod-portal/factorio-mod-portal.service.js new file mode 100644 index 0000000000..27217d267e --- /dev/null +++ b/services/factorio-mod-portal/factorio-mod-portal.service.js @@ -0,0 +1,173 @@ +import Joi from 'joi' +import { BaseJsonService } from '../index.js' +import { age } from '../color-formatters.js' +import { formatDate } from '../text-formatters.js' +import { nonNegativeInteger } from '../validators.js' +import { renderDownloadsBadge } from '../downloads.js' +import { renderVersionBadge } from '../version.js' + +const schema = Joi.object({ + downloads_count: nonNegativeInteger, + releases: Joi.array() + .items( + Joi.object({ + version: Joi.string().required(), + released_at: Joi.string().required(), + info_json: Joi.object({ + factorio_version: Joi.string().required(), + }).required(), + }) + ) + .min(1) + .required(), +}).required() + +// Factorio Mod portal API +// @see https://wiki.factorio.com/Mod_portal_API +class BaseFactorioModPortalService extends BaseJsonService { + async fetch({ modName }) { + const { releases, downloads_count } = await this._requestJson({ + schema, + url: `https://mods.factorio.com/api/mods/${modName}`, + errorMessages: { + 404: 'mod not found', + }, + }) + + return { + downloads_count, + latest_release: releases[releases.length - 1], + } + } +} + +// Badge for mod's latest updated version +class FactorioModPortalLatestVersion extends BaseFactorioModPortalService { + static category = 'version' + + static route = { + base: 'factorio-mod-portal/v', + pattern: ':modName', + } + + static examples = [ + { + title: 'Factorio Mod Portal mod version', + namedParams: { modName: 'rso-mod' }, + staticPreview: this.render({ version: '6.2.20' }), + }, + ] + + static defaultBadgeData = { label: 'latest version' } + + static render({ version }) { + return renderVersionBadge({ version }) + } + + async handle({ modName }) { + const { latest_release } = await this.fetch({ modName }) + return this.constructor.render({ version: latest_release.version }) + } +} + +// Badge for mod's latest compatible Factorio version +class FactorioModPortalFactorioVersion extends BaseFactorioModPortalService { + static category = 'platform-support' + + static route = { + base: 'factorio-mod-portal/factorio-version', + pattern: ':modName', + } + + static examples = [ + { + title: 'Factorio Mod Portal factorio versions', + namedParams: { modName: 'rso-mod' }, + staticPreview: this.render({ version: '1.1' }), + }, + ] + + static defaultBadgeData = { label: 'factorio version' } + + static render({ version }) { + return renderVersionBadge({ version }) + } + + async handle({ modName }) { + const { latest_release } = await this.fetch({ modName }) + const version = latest_release.info_json.factorio_version + return this.constructor.render({ version }) + } +} + +// Badge for mod's last updated date +class FactorioModPortalLastUpdated extends BaseFactorioModPortalService { + static category = 'activity' + + static route = { + base: 'factorio-mod-portal/last-updated', + pattern: ':modName', + } + + static examples = [ + { + title: 'Factorio Mod Portal mod', + namedParams: { modName: 'rso-mod' }, + staticPreview: this.render({ + last_updated: new Date(), + }), + }, + ] + + static defaultBadgeData = { label: 'last updated' } + + static render({ last_updated }) { + return { + message: formatDate(last_updated), + color: age(last_updated), + } + } + + async handle({ modName }) { + const { latest_release } = await this.fetch({ modName }) + return this.constructor.render({ last_updated: latest_release.released_at }) + } +} + +// Badge for mod's total download count +class FactorioModPortalDownloads extends BaseFactorioModPortalService { + static category = 'downloads' + + static route = { + base: 'factorio-mod-portal/dt', + pattern: ':modName', + } + + static examples = [ + { + title: 'Factorio Mod Portal mod downloads', + namedParams: { modName: 'rso-mod' }, + staticPreview: this.render({ + downloads_count: 1694763, + }), + }, + ] + + static defaultBadgeData = { label: 'downloads' } + + static render({ downloads_count }) { + return renderDownloadsBadge({ downloads: downloads_count }) + } + + async handle({ modName }) { + const { downloads_count } = await this.fetch({ modName }) + return this.constructor.render({ downloads_count }) + } +} + +export { + FactorioModPortalLatestVersion, + FactorioModPortalLastUpdated, + FactorioModPortalFactorioVersion, + FactorioModPortalDownloads, +} diff --git a/services/factorio-mod-portal/factorio-mod-portal.tester.js b/services/factorio-mod-portal/factorio-mod-portal.tester.js new file mode 100644 index 0000000000..1c06399116 --- /dev/null +++ b/services/factorio-mod-portal/factorio-mod-portal.tester.js @@ -0,0 +1,47 @@ +import { + isVPlusDottedVersionNClauses, + isFormattedDate, + isMetric, +} from '../test-validators.js' +import { ServiceTester } from '../tester.js' + +export const t = new ServiceTester({ + id: 'factorio-mod-portal', + title: 'Factorio Mod Portal', +}) + +t.create('Latest Version (rso-mod, valid)').get('/v/rso-mod.json').expectBadge({ + label: 'latest version', + message: isVPlusDottedVersionNClauses, +}) + +t.create('Latest Version (mod not found)') + .get('/v/mod-that-doesnt-exist.json') + .expectBadge({ label: 'latest version', message: 'mod not found' }) + +t.create('Factorio Version (rso-mod, valid)') + .get('/factorio-version/rso-mod.json') + .expectBadge({ + label: 'factorio version', + message: isVPlusDottedVersionNClauses, + }) + +t.create('Factorio Version (mod not found)') + .get('/factorio-version/mod-that-doesnt-exist.json') + .expectBadge({ label: 'factorio version', message: 'mod not found' }) + +t.create('Last Updated (rso-mod, valid)') + .get('/last-updated/rso-mod.json') + .expectBadge({ label: 'last updated', message: isFormattedDate }) + +t.create('Last Updated (mod not found)') + .get('/last-updated/mod-that-doesnt-exist.json') + .expectBadge({ label: 'last updated', message: 'mod not found' }) + +t.create('Downloads (rso-mod, valid)') + .get('/dt/rso-mod.json') + .expectBadge({ label: 'downloads', message: isMetric }) + +t.create('Downloads (mod not found)') + .get('/dt/mod-that-doesnt-exist.json') + .expectBadge({ label: 'downloads', message: 'mod not found' })