'use strict'; const { NotFound, InvalidResponse, Inaccessible, } = require('./errors'); const { makeLogo, toArray, makeColor, setBadgeColor, } = require('../lib/badge-data'); module.exports = class BaseService { constructor({ sendAndCacheRequest }, { handleInternalErrors }) { this._sendAndCacheRequest = sendAndCacheRequest; this._handleInternalErrors = handleInternalErrors; } /** * Asynchronous function to handle requests for this service. Takes the URL * parameters (as defined in the `url` property), performs a request using * `this._sendAndCacheRequest`, and returns the badge data. */ async handle(namedParams) { throw new Error( `Handler not implemented for ${this.constructor.name}` ); } // Metadata /** * Name of the category to sort this badge into (eg. "build"). Used to sort * the badges on the main shields.io website. */ static get category() { return 'unknown'; } /** * Returns an object: * - base: (Optional) The base path of the URLs for this service. This is * used as a prefix. * - format: Regular expression to use for URLs for this service's badges * - capture: Array of names for the capture groups in the regular * expression. The handler will be passed an object containing * the matches. */ static get url() { throw new Error(`URL not defined for ${this.name}`); } /** * Default data for the badge. Can include things such as default logo, color, * etc. These defaults will be used if the value is not explicitly overridden * by either the handler or by the user via URL parameters. */ static get defaultBadgeData() { return {}; } /** * Example URLs for this service. These should use the format * specified in `url`, and can be used to demonstrate how to use badges for * this service. */ static get examples() { return []; } static _makeFullUrl(partialUrl) { return '/' + [this.url.base, partialUrl].filter(Boolean).join('/'); } /** * Return an array of examples. Each example is prepared according to the * schema in `lib/all-badge-examples.js`. Four keys are supported: * - title * - previewUrl * - exampleUrl * - documentation */ static prepareExamples() { return this.examples.map(({ title, previewUrl, exampleUrl, documentation }) => { if (! previewUrl) { throw Error(`Example for ${this.name} is missing required previewUrl`); } return { title: title ? `${title}` : this.name, previewUri: `${this._makeFullUrl(previewUrl)}.svg`, exampleUri: exampleUrl ? `${this._makeFullUrl(exampleUrl)}.svg` : undefined, documentation, }; }); } static get _regex() { // Regular expressions treat "/" specially, so we need to escape them const escapedPath = this.url.format.replace(/\//g, '\\/'); const fullRegex = `^${this._makeFullUrl(escapedPath)}.(svg|png|gif|jpg|json)$`; return new RegExp(fullRegex); } static _namedParamsForMatch(match) { // Assume the last match is the format, and drop match[0], which is the // entire match. const captures = match.slice(1, -1); if (this.url.capture.length !== captures.length) { throw new Error( `Service ${this.constructor.name} declares incorrect number of capture groups `+ `(expected ${this.url.capture.length}, got ${captures.length})` ); } const result = {}; this.url.capture.forEach((name, index) => { result[name] = captures[index]; }); return result; } async invokeHandler(namedParams) { try { return await this.handle(namedParams); } catch (error) { if (error instanceof NotFound) { return { message: error.prettyMessage, color: 'red', }; } else if (error instanceof InvalidResponse || error instanceof Inaccessible) { return { message: error.prettyMessage, color: 'lightgray', }; } else if (this._handleInternalErrors) { console.log(error); return { label: 'shields', message: 'internal error', color: 'lightgray', }; } else { throw error; } } } static _makeBadgeData(overrides, serviceData) { const { style, label: overrideLabel, logo: overrideLogo, logoWidth: overrideLogoWidth, link: overrideLink, colorA: overrideColorA, colorB: overrideColorB, } = overrides; const { label: serviceLabel, message: serviceMessage, color: serviceColor, link: serviceLink, } = serviceData; const { color: defaultColor, logo: defaultLogo, label: defaultLabel, } = this.defaultBadgeData; const badgeData = { text: [ overrideLabel || serviceLabel || defaultLabel || this.category, serviceMessage || 'n/a', ], template: style, logo: makeLogo(style === 'social' ? defaultLogo : undefined, { logo: overrideLogo }), logoWidth: +overrideLogoWidth, links: toArray(overrideLink || serviceLink), colorA: makeColor(overrideColorA), }; const color = overrideColorB || serviceColor || defaultColor || 'lightgrey'; setBadgeColor(badgeData, color); return badgeData; } static register(camp, handleRequest, { handleInternalErrors }) { const ServiceClass = this; // In a static context, "this" is the class. camp.route(this._regex, handleRequest(async (queryParams, match, sendBadge, request) => { const namedParams = this._namedParamsForMatch(match); const serviceInstance = new ServiceClass({ sendAndCacheRequest: request.asPromise, }, { handleInternalErrors }); const serviceData = await serviceInstance.invokeHandler(namedParams); const badgeData = this._makeBadgeData(queryParams, serviceData); // Assumes the final capture group is the extension const format = match.slice(-1)[0]; sendBadge(format, badgeData); })); } };