diff --git a/gh-badges/lib/make-badge.js b/gh-badges/lib/make-badge.js index a86622a096f853f7073d7c6b0194cfc07d0f51be..cbd8af8477b7f1a214824008819ecafb5e167e01 100644 --- a/gh-badges/lib/make-badge.js +++ b/gh-badges/lib/make-badge.js @@ -105,7 +105,6 @@ function assignColor(color = '', colorschemeType = 'colorB') { const definedColorschemes = require(path.join(__dirname, 'colorscheme.json')) -// Inject the measurer to avoid placing any persistent state in this module. function makeBadge({ format, template, diff --git a/lib/request-handler.js b/lib/request-handler.js index cfca8780e229b9db1040ddc094eae70f95dae17b..7fc717b278eb900cf0ba41b68c3900b1d339c9c5 100644 --- a/lib/request-handler.js +++ b/lib/request-handler.js @@ -62,13 +62,13 @@ function getBadgeMaxAge(handlerOptions, queryParams) { ? parseInt(process.env.BADGE_MAX_AGE_SECONDS) : 120 if (handlerOptions.cacheLength) { - // if we've set a more specific cache length for this badge (or category), - // use that instead of env.BADGE_MAX_AGE_SECONDS - maxAge = parseInt(handlerOptions.cacheLength) + // If we've set a more specific cache length for this badge (or category), + // use that instead of env.BADGE_MAX_AGE_SECONDS. + maxAge = handlerOptions.cacheLength } if (isInt(queryParams.maxAge) && parseInt(queryParams.maxAge) > maxAge) { - // only allow queryParams.maxAge to override the default - // if it is greater than the default + // Only allow queryParams.maxAge to override the default if it is greater + // than the default. maxAge = parseInt(queryParams.maxAge) } return maxAge diff --git a/server.js b/server.js index d386545070a743314641ff514480928a0c487a4d..ea256dbed56b99e3038fa74b6eb531333e704c2b 100644 --- a/server.js +++ b/server.js @@ -22,8 +22,6 @@ const log = require('./lib/log') const makeBadge = require('./gh-badges/lib/make-badge') const suggest = require('./lib/suggest') const { - makeColorB, - makeLabel: getLabel, makeBadgeData: getBadgeData, setBadgeColor, } = require('./lib/badge-data') @@ -33,7 +31,6 @@ const { } = require('./lib/request-handler') const { clearRegularUpdateCache } = require('./lib/regular-update') const { makeSend } = require('./lib/result-sender') -const { escapeFormat } = require('./lib/path-helpers') const serverStartTime = new Date(new Date().toGMTString()) @@ -107,7 +104,10 @@ camp.notfound(/.*/, (query, match, end, request) => { loadServiceClasses().forEach(serviceClass => serviceClass.register( { camp, handleRequest: cache, githubApiProvider }, - { handleInternalErrors: config.handleInternalErrors } + { + handleInternalErrors: config.handleInternalErrors, + profiling: config.profiling, + } ) ) @@ -227,53 +227,6 @@ camp.route( }) ) -// Any badge. -camp.route( - /^\/(:|badge\/)(([^-]|--)*?)-?(([^-]|--)*)-(([^-]|--)+)\.(svg|png|gif|jpg)$/, - (data, match, end, ask) => { - const subject = escapeFormat(match[2]) - const status = escapeFormat(match[4]) - const color = escapeFormat(match[6]) - const format = match[8] - - analytics.noteRequest(data, match) - - // Cache management - the badge is constant. - const cacheDuration = (3600 * 24 * 1) | 0 // 1 day. - ask.res.setHeader('Cache-Control', `max-age=${cacheDuration}`) - if (+new Date(ask.req.headers['if-modified-since']) >= +serverStartTime) { - ask.res.statusCode = 304 - ask.res.end() // not modified. - return - } - ask.res.setHeader('Last-Modified', serverStartTime.toGMTString()) - - // Badge creation. - try { - const badgeData = getBadgeData(subject, data) - badgeData.text[0] = getLabel(undefined, { label: subject }) - badgeData.text[1] = status - badgeData.colorB = makeColorB(color, data) - badgeData.template = data.style - if (config.profiling.makeBadge) { - console.time('makeBadge total') - } - const svg = makeBadge(badgeData) - if (config.profiling.makeBadge) { - console.timeEnd('makeBadge total') - } - makeSend(format, ask.res, end)(svg) - } catch (e) { - log.error(e.stack) - const svg = makeBadge({ - text: ['error', 'bad badge'], - colorscheme: 'red', - }) - makeSend(format, ask.res, end)(svg) - } - } -) - // Production cache debugging. let bitFlip = false camp.route(/^\/flip\.svg$/, (data, match, end, ask) => { diff --git a/services/base-static.js b/services/base-static.js new file mode 100644 index 0000000000000000000000000000000000000000..cbaac7ea03b3a661dd0b5cbd282f0659a5d9e8e4 --- /dev/null +++ b/services/base-static.js @@ -0,0 +1,58 @@ +'use strict' + +const makeBadge = require('../gh-badges/lib/make-badge') +const { makeSend } = require('../lib/result-sender') +const analytics = require('../lib/analytics') +const BaseService = require('./base') + +const serverStartTime = new Date(new Date().toGMTString()) + +module.exports = class BaseStaticService extends BaseService { + // Note: Since this is a static service, it is not `async`. + handle(namedParams, queryParams) { + throw new Error(`Handler not implemented for ${this.constructor.name}`) + } + + static register({ camp }, serviceConfig) { + camp.route(this._regex, (queryParams, match, end, ask) => { + analytics.noteRequest(queryParams, match) + + if (+new Date(ask.req.headers['if-modified-since']) >= +serverStartTime) { + // Send Not Modified. + ask.res.statusCode = 304 + ask.res.end() + return + } + + const serviceInstance = new this({}, serviceConfig) + const namedParams = this._namedParamsForMatch(match) + let serviceData + try { + // Note: no `await`. + serviceData = serviceInstance.handle(namedParams, queryParams) + } catch (error) { + serviceData = serviceInstance._handleError(error) + } + + const badgeData = this._makeBadgeData(queryParams, serviceData) + + // The final capture group is the extension. + const format = match.slice(-1)[0] + badgeData.format = format + + if (serviceConfig.profiling.makeBadge) { + console.time('makeBadge total') + } + const svg = makeBadge(badgeData) + if (serviceConfig.profiling.makeBadge) { + console.timeEnd('makeBadge total') + } + + const cacheDuration = 3600 * 24 * 1 // 1 day. + ask.res.setHeader('Cache-Control', `max-age=${cacheDuration}`) + ask.res.setHeader('Last-Modified', serverStartTime.toGMTString()) + + makeSend(format, ask.res, end)(svg) + }) + } +} diff --git a/services/base.js b/services/base.js index 87869ddefb56d1b8830ecf4cbf982d9bbce26bde..f3d7c543afddc4cd9c259fd252a6a39b5e34a360 100644 --- a/services/base.js +++ b/services/base.js @@ -22,6 +22,10 @@ const { const { staticBadgeUrl } = require('../lib/make-badge-url') const trace = require('./trace') +function coalesce(...candidates) { + return candidates.find(c => typeof c === 'string') +} + class BaseService { constructor({ sendAndCacheRequest }, { handleInternalErrors }) { this._requestFetcher = sendAndCacheRequest @@ -248,6 +252,52 @@ class BaseService { return result } + _handleError(error) { + if (error instanceof NotFound || error instanceof InvalidParameter) { + trace.logTrace('outbound', emojic.noGoodWoman, 'Handled error', error) + return { + message: error.prettyMessage, + color: 'red', + } + } else if ( + error instanceof InvalidResponse || + error instanceof Inaccessible || + error instanceof Deprecated + ) { + trace.logTrace('outbound', emojic.noGoodWoman, 'Handled error', error) + return { + message: error.prettyMessage, + color: 'lightgray', + } + } else if (this._handleInternalErrors) { + if ( + !trace.logTrace( + 'unhandledError', + emojic.boom, + 'Unhandled internal error', + error + ) + ) { + // This is where we end up if an unhandled exception is thrown in + // production. Send the error to the logs. + console.log(error) + } + return { + label: 'shields', + message: 'internal error', + color: 'lightgray', + } + } else { + trace.logTrace( + 'unhandledError', + emojic.boom, + 'Unhandled internal error', + error + ) + throw error + } + } + async invokeHandler(namedParams, queryParams) { trace.logTrace( 'inbound', @@ -260,49 +310,7 @@ class BaseService { try { return await this.handle(namedParams, queryParams) } catch (error) { - if (error instanceof NotFound || error instanceof InvalidParameter) { - trace.logTrace('outbound', emojic.noGoodWoman, 'Handled error', error) - return { - message: error.prettyMessage, - color: 'red', - } - } else if ( - error instanceof InvalidResponse || - error instanceof Inaccessible || - error instanceof Deprecated - ) { - trace.logTrace('outbound', emojic.noGoodWoman, 'Handled error', error) - return { - message: error.prettyMessage, - color: 'lightgray', - } - } else if (this._handleInternalErrors) { - if ( - !trace.logTrace( - 'unhandledError', - emojic.boom, - 'Unhandled internal error', - error - ) - ) { - // This is where we end up if an unhandled exception is thrown in - // production. Send the error to the logs. - console.log(error) - } - return { - label: 'shields', - message: 'internal error', - color: 'lightgray', - } - } else { - trace.logTrace( - 'unhandledError', - emojic.boom, - 'Unhandled internal error', - error - ) - throw error - } + return this._handleError(error) } } @@ -333,8 +341,10 @@ class BaseService { const badgeData = { text: [ - overrideLabel || serviceLabel || defaultLabel || this.category, - serviceMessage || 'n/a', + // Use `coalesce()` to support empty labels and messages, as in the + // static badge. + coalesce(overrideLabel, serviceLabel, defaultLabel, this.category), + coalesce(serviceMessage, 'n/a'), ], template: style, logo: makeLogo(style === 'social' ? defaultLogo : undefined, { @@ -352,15 +362,12 @@ class BaseService { } static register({ camp, handleRequest, githubApiProvider }, serviceConfig) { - const ServiceClass = this // In a static context, "this" is the class. - camp.route( this._regex, handleRequest({ queryParams: this.route.queryParams, handler: async (queryParams, match, sendBadge, request) => { - const namedParams = this._namedParamsForMatch(match) - const serviceInstance = new ServiceClass( + const serviceInstance = new this( { sendAndCacheRequest: request.asPromise, sendAndCacheRequestWithCallbacks: request, @@ -368,6 +375,7 @@ class BaseService { }, serviceConfig ) + const namedParams = this._namedParamsForMatch(match) const serviceData = await serviceInstance.invokeHandler( namedParams, queryParams @@ -375,7 +383,7 @@ class BaseService { trace.logTrace('outbound', emojic.shield, 'Service data', serviceData) const badgeData = this._makeBadgeData(queryParams, serviceData) - // Assumes the final capture group is the extension + // The final capture group is the extension. const format = match.slice(-1)[0] sendBadge(format, badgeData) }, diff --git a/services/gitter/gitter.service.js b/services/gitter/gitter.service.js index e6913b3b73d93432d0788916fc190c8a70595c25..1abb3ff4a4b76a912956f381829745ceaa115cea 100644 --- a/services/gitter/gitter.service.js +++ b/services/gitter/gitter.service.js @@ -1,38 +1,39 @@ 'use strict' -const LegacyService = require('../legacy-service') -const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const BaseStaticService = require('../base-static') -module.exports = class Gitter extends LegacyService { +module.exports = class Gitter extends BaseStaticService { static get category() { return 'chat' } static get route() { - return { base: 'gitter/room' } + return { + base: 'gitter/room', + pattern: ':user/:repo', + } } static get examples() { return [ { title: 'Gitter', - previewUrl: 'nwjs/nw.js', + urlPattern: ':user/:repo', + staticExample: this.render(), + exampleUrl: 'nwjs/nw.js', }, ] } - static registerLegacyRouteHandler({ camp, cache }) { - camp.route( - /^\/gitter\/room\/([^/]+\/[^/]+)\.(svg|png|gif|jpg|json)$/, - cache((data, match, sendBadge, request) => { - // match[1] is the repo, which is not used. - const format = match[2] + static get defaultBadgeData() { + return { label: 'chat' } + } + + static render() { + return { message: 'on gitter', color: 'brightgreen' } + } - const badgeData = getBadgeData('chat', data) - badgeData.text[1] = 'on gitter' - badgeData.colorscheme = 'brightgreen' - sendBadge(format, badgeData) - }) - ) + handle() { + return this.constructor.render() } } diff --git a/services/static-badge/static-badge.service.js b/services/static-badge/static-badge.service.js new file mode 100644 index 0000000000000000000000000000000000000000..e6211378399d7cb16c50458e81e41ac94d31bcb0 --- /dev/null +++ b/services/static-badge/static-badge.service.js @@ -0,0 +1,20 @@ +'use strict' + +const BaseStaticService = require('../base-static') + +module.exports = class StaticBadge extends BaseStaticService { + static get category() { + return 'other' + } + + static get route() { + return { + format: '(?::|badge/)((?:[^-]|--)*?)-?((?:[^-]|--)*)-((?:[^-]|--)+)', + capture: ['label', 'message', 'color'], + } + } + + handle({ label, message, color }) { + return { label, message, color } + } +} diff --git a/services/static-badge/static-badge.tester.js b/services/static-badge/static-badge.tester.js new file mode 100644 index 0000000000000000000000000000000000000000..13371ea3afdacf8b28cc3f38a3685a965034925c --- /dev/null +++ b/services/static-badge/static-badge.tester.js @@ -0,0 +1,40 @@ +'use strict' + +const t = require('../create-service-tester')() +module.exports = t + +t.create('Shields colorscheme color') + .get('/badge/label-message-blue.json?style=_shields_test') + .expectJSON({ name: 'label', value: 'message', colorB: '#007ec6' }) + +t.create('CSS named color') + .get('/badge/label-message-whitesmoke.json?style=_shields_test') + .expectJSON({ name: 'label', value: 'message', colorB: 'whitesmoke' }) + +t.create('RGB color') + .get('/badge/label-message-rgb(123,123,123).json?style=_shields_test') + .expectJSON({ name: 'label', value: 'message', colorB: 'rgb(123,123,123)' }) + +t.create('All one color') + .get('/badge/all%20one%20color-red.json?style=_shields_test') + .expectJSON({ name: '', value: 'all one color', colorB: '#e05d44' }) + +t.create('Not a valid color') + .get('/badge/label-message-notacolor.json?style=_shields_test') + .expectJSON({ name: 'label', value: 'message', colorB: '#e05d44' }) + +t.create('Missing message') + .get('/badge/label--blue.json?style=_shields_test') + .expectJSON({ name: 'label', value: '', colorB: '#007ec6' }) + +t.create('Missing label') + .get('/badge/-message-blue.json?style=_shields_test') + .expectJSON({ name: '', value: 'message', colorB: '#007ec6' }) + +t.create('Override colorB') + .get('/badge/label-message-blue.json?style=_shields_test&colorB=yellow') + .expectJSON({ name: 'label', value: 'message', colorB: '#dfb317' }) + +t.create('Override label') + .get('/badge/label-message-blue.json?style=_shields_test&label=mylabel') + .expectJSON({ name: 'mylabel', value: 'message', colorB: '#007ec6' })