Files
shields/services/base.js
Paul Melnikow f2efd751b5 Minor refactor of examples preparation (#1632)
This cleans up the work from #1582, clarifying concerns, removing a bit of duplication, and renaming for clarity.
2018-04-02 07:03:52 -05:00

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 ? `${this.name} ${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);
}));
}
};