* allow services to export >1 classes
This change to loadServiceClasses() allows us to define
services which either export a single service class e.g:
module.exports = class Cdnjs extends BaseService {
//...
}
or more than one. e.g:
module.exports = {
GemVersion,
GemDownloads,
GemOwner,
GemRank,
}
* refactor ruby gem badges
- move badge code to service classes
- throw exceptions for errors
- use let and const
- change tests to expect 'downloads' label for error badges
- general tidying
* fix typo in tests
* Don't always use class name in example label
This allows (for example) GemVersion and GemDownloads
both to use the example label 'Gem'
213 lines
6.0 KiB
JavaScript
213 lines
6.0 KiB
JavaScript
'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);
|
|
}));
|
|
}
|
|
};
|