diff --git a/core/base-service/base-non-memory-caching.js b/core/base-service/base-non-memory-caching.js index db457ae58aad7..0b442e5aa50eb 100644 --- a/core/base-service/base-non-memory-caching.js +++ b/core/base-service/base-non-memory-caching.js @@ -24,11 +24,15 @@ const { prepareRoute, namedParamsForMatch } = require('./route') // configured by the service, the user's request, and the server's default // cache length. module.exports = class NonMemoryCachingBaseService extends BaseService { - static register({ camp }, serviceConfig) { + static register({ camp, requestCounter }, serviceConfig) { const { cacheHeaders: cacheHeaderConfig } = serviceConfig const { _cacheLength: serviceDefaultCacheLengthSeconds } = this const { regex, captureNames } = prepareRoute(this.route) + const serviceRequestCounter = this._createServiceRequestCounter({ + requestCounter, + }) + camp.route(regex, async (queryParams, match, end, ask) => { const namedParams = namedParamsForMatch(captureNames, match, this) const serviceData = await this.invoke( @@ -59,6 +63,8 @@ module.exports = class NonMemoryCachingBaseService extends BaseService { }) makeSend(format, ask.res, end)(svg) + + serviceRequestCounter.inc() }) } } diff --git a/core/base-service/base-static.js b/core/base-service/base-static.js index 094040a935ed8..d784054765fb3 100644 --- a/core/base-service/base-static.js +++ b/core/base-service/base-static.js @@ -12,12 +12,16 @@ const coalesceBadge = require('./coalesce-badge') const { prepareRoute, namedParamsForMatch } = require('./route') module.exports = class BaseStaticService extends BaseService { - static register({ camp }, serviceConfig) { + static register({ camp, requestCounter }, serviceConfig) { const { profiling: { makeBadge: shouldProfileMakeBadge }, } = serviceConfig const { regex, captureNames } = prepareRoute(this.route) + const serviceRequestCounter = this._createServiceRequestCounter({ + requestCounter, + }) + camp.route(regex, async (queryParams, match, end, ask) => { analytics.noteRequest(queryParams, match) @@ -58,6 +62,8 @@ module.exports = class BaseStaticService extends BaseService { setCacheHeadersForStaticResource(ask.res) makeSend(format, ask.res, end)(svg) + + serviceRequestCounter.inc() }) } } diff --git a/core/base-service/base.js b/core/base-service/base.js index 9da4d61685377..f8460958d1ad3 100644 --- a/core/base-service/base.js +++ b/core/base-service/base.js @@ -1,5 +1,6 @@ 'use strict' +const decamelize = require('decamelize') // See available emoji at http://emoji.muan.co/ const emojic = require('emojic') const Joi = require('joi') @@ -317,11 +318,29 @@ module.exports = class BaseService { return serviceData } - static register({ camp, handleRequest, githubApiProvider }, serviceConfig) { + static _createServiceRequestCounter({ requestCounter }) { + if (requestCounter) { + const { category, serviceFamily, name } = this + const service = decamelize(name) + return requestCounter.labels(category, serviceFamily, service) + } else { + // When metrics are disabled, return a mock counter. + return { inc: () => {} } + } + } + + static register( + { camp, handleRequest, githubApiProvider, requestCounter }, + serviceConfig + ) { const { cacheHeaders: cacheHeaderConfig, fetchLimitBytes } = serviceConfig const { regex, captureNames } = prepareRoute(this.route) const queryParams = getQueryParamNames(this.route) + const serviceRequestCounter = this._createServiceRequestCounter({ + requestCounter, + }) + camp.route( regex, handleRequest(cacheHeaderConfig, { @@ -348,6 +367,8 @@ module.exports = class BaseService { // The final capture group is the extension. const format = match.slice(-1)[0] sendBadge(format, badgeData) + + serviceRequestCounter.inc() }, cacheLength: this._cacheLength, fetchLimitBytes, diff --git a/core/base-service/categories.js b/core/base-service/categories.js index 1068ab3d95453..36caaa6c9109e 100644 --- a/core/base-service/categories.js +++ b/core/base-service/categories.js @@ -14,5 +14,6 @@ function assertValidCategory(category, message = undefined) { } module.exports = { + isValidCategory, assertValidCategory, } diff --git a/core/base-service/deprecated-service.js b/core/base-service/deprecated-service.js index b774f6dc519ac..f81c2014a5e94 100644 --- a/core/base-service/deprecated-service.js +++ b/core/base-service/deprecated-service.js @@ -1,19 +1,40 @@ 'use strict' const Joi = require('joi') +const camelcase = require('camelcase') const BaseService = require('./base') +const { isValidCategory } = require('./categories') const { Deprecated } = require('./errors') +const { isValidRoute } = require('./route') + +const attrSchema = Joi.object({ + route: isValidRoute, + name: Joi.string(), + label: Joi.string(), + category: isValidCategory, + // The content of examples is validated later, via `transformExamples()`. + examples: Joi.array().default([]), + message: Joi.string(), + dateAdded: Joi.date().required(), +}).required() + +function deprecatedService(attrs) { + const { route, name, label, category, examples, message } = Joi.attempt( + attrs, + attrSchema, + `Deprecated service for ${attrs.route.base}` + ) -// Only `url` is required. -function deprecatedService({ - route, - label, - category, - examples = [], - message, - dateAdded, -}) { return class DeprecatedService extends BaseService { + static get name() { + return ( + name || + `Deprecated${camelcase(route.base.replace(/\//g, '_'), { + pascalCase: true, + })}` + ) + } + static get category() { return category } @@ -26,17 +47,6 @@ function deprecatedService({ return true } - static validateDefinition() { - super.validateDefinition() - Joi.assert( - { dateAdded }, - Joi.object({ - dateAdded: Joi.date().required(), - }), - `Deprecated service for ${route.base}` - ) - } - static get defaultBadgeData() { return { label } } diff --git a/core/base-service/deprecated-service.spec.js b/core/base-service/deprecated-service.spec.js index 03c2682132bd0..d448bfde452f4 100644 --- a/core/base-service/deprecated-service.spec.js +++ b/core/base-service/deprecated-service.spec.js @@ -5,29 +5,36 @@ const deprecatedService = require('./deprecated-service') describe('DeprecatedService', function() { const route = { - base: 'coverity/ondemand', + base: 'service/that/no/longer/exists', format: '(?:.+)', } + const category = 'analysis' + const dateAdded = new Date() + const commonAttrs = { route, category, dateAdded } it('returns true on isDeprecated', function() { - const service = deprecatedService({ route }) + const service = deprecatedService({ ...commonAttrs }) expect(service.isDeprecated).to.be.true }) + it('has the expected name', function() { + const service = deprecatedService({ ...commonAttrs }) + expect(service.name).to.equal('DeprecatedServiceThatNoLongerExists') + }) + it('sets specified route', function() { - const service = deprecatedService({ route }) + const service = deprecatedService({ ...commonAttrs }) expect(service.route).to.deep.equal(route) }) it('sets specified label', function() { const label = 'coverity' - const service = deprecatedService({ route, label }) + const service = deprecatedService({ ...commonAttrs, label }) expect(service.defaultBadgeData.label).to.equal(label) }) it('sets specified category', function() { - const category = 'analysis' - const service = deprecatedService({ route, category }) + const service = deprecatedService({ ...commonAttrs }) expect(service.category).to.equal(category) }) @@ -37,12 +44,12 @@ describe('DeprecatedService', function() { title: 'Not sure we would have examples', }, ] - const service = deprecatedService({ route, examples }) + const service = deprecatedService({ ...commonAttrs, examples }) expect(service.examples).to.deep.equal(examples) }) it('uses default deprecation message when no message specified', async function() { - const service = deprecatedService({ route }) + const service = deprecatedService({ ...commonAttrs }) expect(await service.invoke()).to.deep.equal({ isError: true, color: 'lightgray', @@ -52,7 +59,7 @@ describe('DeprecatedService', function() { it('uses custom deprecation message when specified', async function() { const message = 'extended outage' - const service = deprecatedService({ route, message }) + const service = deprecatedService({ ...commonAttrs, message }) expect(await service.invoke()).to.deep.equal({ isError: true, color: 'lightgray', diff --git a/core/base-service/loader.js b/core/base-service/loader.js index 6b31bb1eb5c53..de442ff12e133 100644 --- a/core/base-service/loader.js +++ b/core/base-service/loader.js @@ -2,6 +2,7 @@ const path = require('path') const glob = require('glob') +const countBy = require('lodash.countby') const categories = require('../../services/categories') const BaseService = require('./base') const { assertValidServiceDefinitionExport } = require('./service-definitions') @@ -20,35 +21,46 @@ function loadServiceClasses(servicePaths) { servicePaths = glob.sync(path.join(serviceDir, '**', '*.service.js')) } - const serviceClasses = [] - servicePaths.forEach(path => { - const module = require(path) + let serviceClasses = [] + servicePaths.forEach(servicePath => { + const module = require(servicePath) + + const theseServiceClasses = [] if ( !module || (module.constructor === Array && module.length === 0) || (module.constructor === Object && Object.keys(module).length === 0) ) { throw new InvalidService( - `Expected ${path} to export a service or a collection of services` + `Expected ${servicePath} to export a service or a collection of services` ) } else if (module.prototype instanceof BaseService) { - serviceClasses.push(module) + theseServiceClasses.push(module) } else if (module.constructor === Array || module.constructor === Object) { for (const key in module) { const serviceClass = module[key] if (serviceClass.prototype instanceof BaseService) { - serviceClasses.push(serviceClass) + theseServiceClasses.push(serviceClass) } else { throw new InvalidService( - `Expected ${path} to export a service or a collection of services; one of them was ${serviceClass}` + `Expected ${servicePath} to export a service or a collection of services; one of them was ${serviceClass}` ) } } } else { throw new InvalidService( - `Expected ${path} to export a service or a collection of services; got ${module}` + `Expected ${servicePath} to export a service or a collection of services; got ${module}` ) } + + // Decorate each service class with the directory that contains it. + theseServiceClasses.forEach(serviceClass => { + serviceClass.serviceFamily = servicePath + .replace(serviceDir, '') + .split(path.sep)[1] + }) + + serviceClasses = serviceClasses.concat(theseServiceClasses) }) serviceClasses.forEach(ServiceClass => ServiceClass.validateDefinition()) @@ -56,6 +68,25 @@ function loadServiceClasses(servicePaths) { return serviceClasses } +function assertNamesUnique(names, { message }) { + const duplicates = {} + Object.entries(countBy(names)) + .filter(([name, count]) => count > 1) + .forEach(([name, count]) => { + duplicates[name] = count + }) + if (Object.keys(duplicates).length) { + throw new Error(`${message}: ${JSON.stringify(duplicates, undefined, 2)}`) + } +} + +function checkNames() { + const services = loadServiceClasses() + assertNamesUnique(services.map(({ name }) => name), { + message: 'Duplicate service names found', + }) +} + function collectDefinitions() { const services = loadServiceClasses() // flatMap. @@ -78,6 +109,7 @@ function loadTesters() { module.exports = { InvalidService, loadServiceClasses, + checkNames, collectDefinitions, loadTesters, } diff --git a/core/base-service/redirector.js b/core/base-service/redirector.js index 07798d67e4aca..ff7843f1038d6 100644 --- a/core/base-service/redirector.js +++ b/core/base-service/redirector.js @@ -1,5 +1,6 @@ 'use strict' +const camelcase = require('camelcase') const emojic = require('emojic') const Joi = require('joi') const BaseService = require('./base') @@ -7,10 +8,30 @@ const { serverHasBeenUpSinceResourceCached, setCacheHeadersForStaticResource, } = require('./cache-headers') -const { prepareRoute, namedParamsForMatch } = require('./route') +const { isValidCategory } = require('./categories') +const { isValidRoute, prepareRoute, namedParamsForMatch } = require('./route') const trace = require('./trace') -module.exports = function redirector({ category, route, target, dateAdded }) { +const attrSchema = Joi.object({ + category: isValidCategory, + route: isValidRoute, + target: Joi.func() + .maxArity(1) + .required() + .error( + () => + '"target" must be a function that transforms named params to a new path' + ), + dateAdded: Joi.date().required(), +}).required() + +module.exports = function redirector(attrs) { + const { category, route, target } = Joi.attempt( + attrs, + attrSchema, + `Redirector for ${attrs.route.base}` + ) + return class Redirector extends BaseService { static get category() { return category @@ -24,20 +45,19 @@ module.exports = function redirector({ category, route, target, dateAdded }) { return true } - static validateDefinition() { - super.validateDefinition() - Joi.assert( - { dateAdded }, - Joi.object({ - dateAdded: Joi.date().required(), - }), - `Redirector for ${route.base}` - ) + static get name() { + return `${camelcase(route.base.replace(/\//g, '_'), { + pascalCase: true, + })}Redirect` } - static register({ camp }) { + static register({ camp, requestCounter }) { const { regex, captureNames } = prepareRoute(this.route) + const serviceRequestCounter = this._createServiceRequestCounter({ + requestCounter, + }) + camp.route(regex, async (queryParams, match, end, ask) => { if (serverHasBeenUpSinceResourceCached(ask.req)) { // Send Not Modified. @@ -72,6 +92,8 @@ module.exports = function redirector({ category, route, target, dateAdded }) { setCacheHeadersForStaticResource(ask.res) ask.res.end() + + serviceRequestCounter.inc() }) } } diff --git a/core/base-service/redirector.spec.js b/core/base-service/redirector.spec.js index 6fb81c5f54818..163604c628409 100644 --- a/core/base-service/redirector.spec.js +++ b/core/base-service/redirector.spec.js @@ -13,17 +13,17 @@ describe('Redirector', function() { } const category = 'analysis' const target = () => {} - const attrs = { - category, - route, - target, - dateAdded: new Date(), - } + const dateAdded = new Date() + const attrs = { category, route, target, dateAdded } it('returns true on isDeprecated', function() { expect(redirector(attrs).isDeprecated).to.be.true }) + it('has the expected name', function() { + expect(redirector(attrs).name).to.equal('VeryOldServiceRedirect') + }) + it('sets specified route', function() { expect(redirector(attrs).route).to.deep.equal(route) }) @@ -60,8 +60,8 @@ describe('Redirector', function() { const target = ({ namedParamA }) => `/new/service/${namedParamA}` beforeEach(function() { - const ServiceClass = redirector({ route, target }) - ServiceClass.register({ camp }) + const ServiceClass = redirector({ category, route, target, dateAdded }) + ServiceClass.register({ camp }, {}) }) it('should redirect as configured', async function() { diff --git a/core/base-service/route.js b/core/base-service/route.js index 711d7d5ca426b..688b7ef430c7d 100644 --- a/core/base-service/route.js +++ b/core/base-service/route.js @@ -7,8 +7,10 @@ function makeFullUrl(base, partialUrl) { return `/${[base, partialUrl].filter(Boolean).join('/')}` } -const routeSchema = Joi.object({ - base: Joi.string().allow(''), +const isValidRoute = Joi.object({ + base: Joi.string() + .allow('') + .required(), pattern: Joi.string().allow(''), format: Joi.string(), capture: Joi.alternatives().when('format', { @@ -23,7 +25,7 @@ const routeSchema = Joi.object({ .required() function assertValidRoute(route, message = undefined) { - Joi.assert(route, routeSchema, message) + Joi.assert(route, isValidRoute, message) } function prepareRoute({ base, pattern, format, capture }) { @@ -80,6 +82,7 @@ function getQueryParamNames({ queryParams = [], queryParamSchema }) { module.exports = { makeFullUrl, + isValidRoute, assertValidRoute, prepareRoute, namedParamsForMatch, diff --git a/core/server/prometheus-metrics.js b/core/server/prometheus-metrics.js index 939dd319444e6..9acd4ccd47dd5 100644 --- a/core/server/prometheus-metrics.js +++ b/core/server/prometheus-metrics.js @@ -3,25 +3,31 @@ const prometheus = require('prom-client') module.exports = class PrometheusMetrics { - constructor(config = {}) { - this.enabled = config.enabled || false - if (this.enabled) { - console.log('Metrics are enabled.') - } + constructor() { + this.register = new prometheus.Registry() + this.requestCounter = new prometheus.Counter({ + name: 'service_requests_total', + help: 'Total service requests', + labelNames: ['category', 'family', 'service'], + registers: [this.register], + }) } async initialize(server) { - if (this.enabled) { - const register = prometheus.register - prometheus.collectDefaultMetrics() - this.setRoutes(server, register) - } - } + const { register } = this + this.interval = prometheus.collectDefaultMetrics({ register }) - setRoutes(server, register) { server.route(/^\/metrics$/, (data, match, end, ask) => { ask.res.setHeader('Content-Type', register.contentType) ask.res.end(register.metrics()) }) } + + stop() { + this.register.clear() + if (this.interval) { + clearInterval(this.interval) + this.interval = undefined + } + } } diff --git a/core/server/prometheus-metrics.spec.js b/core/server/prometheus-metrics.spec.js index e69f2eb0d8bee..32cdf006228ba 100644 --- a/core/server/prometheus-metrics.spec.js +++ b/core/server/prometheus-metrics.spec.js @@ -25,25 +25,7 @@ describe('Prometheus metrics route', function() { } }) - it('returns 404 when metrics are disabled', async function() { - new Metrics({ enabled: false }).initialize(camp) - - const res = await fetch(`${baseUrl}/metrics`) - - expect(res.status).to.be.equal(404) - expect(await res.text()).to.not.contains('nodejs_version_info') - }) - - it('returns 404 when there is no configuration', async function() { - new Metrics().initialize(camp) - - const res = await fetch(`${baseUrl}/metrics`) - - expect(res.status).to.be.equal(404) - expect(await res.text()).to.not.contains('nodejs_version_info') - }) - - it('returns metrics when enabled', async function() { + it('returns metrics', async function() { new Metrics({ enabled: true }).initialize(camp) const res = await fetch(`${baseUrl}/metrics`) diff --git a/core/server/server.js b/core/server/server.js index ea381f583e70a..4186a761d41eb 100644 --- a/core/server/server.js +++ b/core/server/server.js @@ -135,7 +135,9 @@ module.exports = class Server { persistence: publicConfig.persistence, service: publicConfig.services.github, }) - this.metrics = new PrometheusMetrics(publicConfig.metrics.prometheus) + if (publicConfig.metrics.prometheus.enabled) { + this.metrics = new PrometheusMetrics() + } } get port() { @@ -182,10 +184,11 @@ module.exports = class Server { registerServices() { const { config, camp } = this const { apiProvider: githubApiProvider } = this.githubConstellation + const { requestCounter } = this.metrics || {} loadServiceClasses().forEach(serviceClass => serviceClass.register( - { camp, handleRequest, githubApiProvider }, + { camp, handleRequest, githubApiProvider, requestCounter }, { handleInternalErrors: config.public.handleInternalErrors, cacheHeaders: config.public.cacheHeaders, @@ -257,7 +260,9 @@ module.exports = class Server { const { githubConstellation, metrics } = this githubConstellation.initialize(camp) - metrics.initialize(camp) + if (metrics) { + metrics.initialize(camp) + } const { apiProvider: githubApiProvider } = this.githubConstellation suggest.setRoutes(allowedOrigin, githubApiProvider, camp) @@ -296,6 +301,10 @@ module.exports = class Server { this.githubConstellation = undefined } + if (this.metrics) { + this.metrics.stop() + } + analytics.cancelAutosaving() } } diff --git a/package-lock.json b/package-lock.json index 06623469ba120..6ee60725fe6e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1472,6 +1472,20 @@ "which-module": "^2.0.0", "y18n": "^3.2.1", "yargs-parser": "^7.0.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + } } }, "yargs-parser": { @@ -1481,6 +1495,14 @@ "dev": true, "requires": { "camelcase": "^4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + } } } } @@ -3216,6 +3238,14 @@ "string-width": "^2.0.0", "term-size": "^1.2.0", "widest-line": "^2.0.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + } } }, "brace-expansion": { @@ -3647,10 +3677,9 @@ "dev": true }, "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==" }, "camelcase-keys": { "version": "4.2.0", @@ -5209,6 +5238,14 @@ "which-module": "^2.0.0", "y18n": "^3.2.1 || ^4.0.0", "yargs-parser": "^11.1.0" + }, + "dependencies": { + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + } } }, "yargs-parser": { @@ -5219,6 +5256,14 @@ "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" + }, + "dependencies": { + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + } } } } @@ -6016,10 +6061,12 @@ } }, "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz", + "integrity": "sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==", + "requires": { + "xregexp": "4.0.0" + } }, "decamelize-keys": { "version": "1.1.0", @@ -6031,6 +6078,12 @@ "map-obj": "^1.0.0" }, "dependencies": { + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, "map-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", @@ -9816,6 +9869,14 @@ "dev": true, "requires": { "decamelize": "^1.0.0" + }, + "dependencies": { + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + } } }, "husky": { @@ -12737,6 +12798,14 @@ "which-module": "^2.0.0", "y18n": "^3.2.1 || ^4.0.0", "yargs-parser": "^11.1.1" + }, + "dependencies": { + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + } } }, "yargs-parser": { @@ -12747,6 +12816,14 @@ "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" + }, + "dependencies": { + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + } } } } @@ -17417,6 +17494,14 @@ "which-module": "^2.0.0", "y18n": "^3.2.1", "yargs-parser": "^8.1.0" + }, + "dependencies": { + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + } } }, "yargs-parser": { @@ -17426,6 +17511,14 @@ "dev": true, "requires": { "camelcase": "^4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + } } } } @@ -20922,6 +21015,14 @@ "dev": true, "requires": { "camelcase": "^4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + } } } } @@ -21196,8 +21297,7 @@ "xregexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", - "integrity": "sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==", - "dev": true + "integrity": "sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==" }, "xtend": { "version": "4.0.1", @@ -21242,6 +21342,14 @@ "which-module": "^2.0.0", "y18n": "^3.2.1", "yargs-parser": "^9.0.2" + }, + "dependencies": { + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + } } }, "yargs-parser": { @@ -21251,6 +21359,14 @@ "dev": true, "requires": { "camelcase": "^4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + } } }, "yargs-unparser": { @@ -21405,6 +21521,14 @@ "which-module": "^2.0.0", "y18n": "^3.2.1 || ^4.0.0", "yargs-parser": "^11.1.1" + }, + "dependencies": { + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + } } }, "yargs-parser": { @@ -21415,6 +21539,14 @@ "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" + }, + "dependencies": { + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + } } } } diff --git a/package.json b/package.json index 897a098e6b75b..ed5efbafc2fee 100644 --- a/package.json +++ b/package.json @@ -23,12 +23,14 @@ }, "dependencies": { "bytes": "^3.1.0", + "camelcase": "^5.0.0", "camp": "~17.2.2", "chalk": "^2.4.2", "check-node-version": "^3.1.0", "chrome-web-store-item-property": "~1.1.2", "config": "^3.0.1", "cross-env": "^5.2.0", + "decamelize": "^2.0.0", "dotenv": "^6.2.0", "emojic": "^1.1.15", "escape-string-regexp": "^1.0.5", @@ -37,8 +39,8 @@ "gh-badges": "file:gh-badges", "glob": "^7.1.1", "joi": "14.3.1", - "js-yaml": "^3.12.1", "joi-extension-semver": "2.0.0", + "js-yaml": "^3.12.1", "jsonpath": "~1.0.0", "lodash.countby": "^4.6.0", "lodash.throttle": "^4.1.1", diff --git a/services/bitbucket/bitbucket-issues.service.js b/services/bitbucket/bitbucket-issues.service.js index c0ed5e6a9b82a..81f0f334aa2e9 100644 --- a/services/bitbucket/bitbucket-issues.service.js +++ b/services/bitbucket/bitbucket-issues.service.js @@ -14,6 +14,10 @@ function issueClassGenerator(raw) { const badgeSuffix = raw ? '' : ' open' return class BitbucketIssues extends BaseJsonService { + static get name() { + return `BitbucketIssues${raw ? 'Raw' : ''}` + } + async fetch({ user, repo }) { const url = `https://bitbucket.org/api/1.0/repositories/${user}/${repo}/issues/` return this._requestJson({ diff --git a/services/bitbucket/bitbucket-pull-request.service.js b/services/bitbucket/bitbucket-pull-request.service.js index f7561ca85a74f..7e8803cb98f1b 100644 --- a/services/bitbucket/bitbucket-pull-request.service.js +++ b/services/bitbucket/bitbucket-pull-request.service.js @@ -19,6 +19,10 @@ function pullRequestClassGenerator(raw) { const badgeSuffix = raw ? '' : ' open' return class BitbucketPullRequest extends BaseJsonService { + static get name() { + return `BitbucketPullRequest${raw ? 'Raw' : ''}` + } + async fetchCloud({ args, user, repo }) { args.url = `https://bitbucket.org/api/2.0/repositories/${user}/${repo}/pullrequests/` args.options = { qs: { state: 'OPEN', limit: 0 } } diff --git a/services/check-services.spec.js b/services/check-services.spec.js index 082f8a6cd7925..605ad0539f3a7 100644 --- a/services/check-services.spec.js +++ b/services/check-services.spec.js @@ -1,9 +1,17 @@ 'use strict' -const { collectDefinitions } = require('../core/base-service/loader') +const { + checkNames, + collectDefinitions, +} = require('../core/base-service/loader') + +// When these tests fail, they will throw AssertionErrors. Wrapping them in an +// `expect().not.to.throw()` makes the error output unreadable. + +it('Services have unique names', function() { + checkNames() +}) it('Can collect the service definitions', function() { - // When this fails, it will throw AssertionErrors. Wrapping this in an - // `expect().not.to.throw()` makes the error output unreadable. collectDefinitions() }) diff --git a/services/chrome-web-store/chrome-web-store.service.js b/services/chrome-web-store/chrome-web-store.service.js index f7ec547c989c9..5b8c0edb30ef4 100644 --- a/services/chrome-web-store/chrome-web-store.service.js +++ b/services/chrome-web-store/chrome-web-store.service.js @@ -151,6 +151,13 @@ class ChromeWebStore extends LegacyService { return 'other' } + static get route() { + return { + base: 'chrome-web-store', + pattern: ':which(v|d|users|price|rating|stars|rating-count)/:storeId', + } + } + static registerLegacyRouteHandler({ camp, cache }) { camp.route( /^\/chrome-web-store\/(v|d|users|price|rating|stars|rating-count)\/(.*)\.(svg|png|gif|jpg|json)$/, diff --git a/services/clojars/clojars-downloads.service.js b/services/clojars/clojars-downloads.service.js index f1282773f224e..7cea2f15ae3cf 100644 --- a/services/clojars/clojars-downloads.service.js +++ b/services/clojars/clojars-downloads.service.js @@ -10,7 +10,7 @@ const clojarsSchema = Joi.object({ downloads: nonNegativeInteger, }).required() -module.exports = class Clojars extends BaseJsonService { +module.exports = class ClojarsDownloads extends BaseJsonService { async fetch({ clojar }) { const url = `https://clojars.org/api/artifacts/${clojar}` return this._requestJson({ diff --git a/services/clojars/clojars-version.service.js b/services/clojars/clojars-version.service.js index deae4dbd0cb34..0b2d99367fd7f 100644 --- a/services/clojars/clojars-version.service.js +++ b/services/clojars/clojars-version.service.js @@ -9,7 +9,7 @@ const clojarsSchema = Joi.object({ version: Joi.string(), }).required() -module.exports = class Clojars extends BaseJsonService { +module.exports = class ClojarsVersion extends BaseJsonService { async fetch({ clojar }) { const url = `https://clojars.org/${clojar}/latest-version.json` return this._requestJson({ diff --git a/services/cocoapods/cocoapods-apps.service.js b/services/cocoapods/cocoapods-apps.service.js index 9734e375f4b66..277cc6a43cdef 100644 --- a/services/cocoapods/cocoapods-apps.service.js +++ b/services/cocoapods/cocoapods-apps.service.js @@ -3,6 +3,7 @@ const { deprecatedService } = require('..') module.exports = deprecatedService({ + name: 'CocoapodsApps', category: 'other', route: { base: 'cocoapods', diff --git a/services/cocoapods/cocoapods-downloads.service.js b/services/cocoapods/cocoapods-downloads.service.js index 777cdd4047cd2..c0caf8da6be81 100644 --- a/services/cocoapods/cocoapods-downloads.service.js +++ b/services/cocoapods/cocoapods-downloads.service.js @@ -3,6 +3,7 @@ const { deprecatedService } = require('..') module.exports = deprecatedService({ + name: 'CocoapodsDownloads', category: 'downloads', route: { base: 'cocoapods', diff --git a/services/conda/conda-version.service.js b/services/conda/conda-version.service.js index 69e4ae1edd16d..c88d5a896f659 100644 --- a/services/conda/conda-version.service.js +++ b/services/conda/conda-version.service.js @@ -4,7 +4,7 @@ const { addv: versionText } = require('../../lib/text-formatters') const { version: versionColor } = require('../../lib/color-formatters') const BaseCondaService = require('./conda-base') -module.exports = class CondaDownloads extends BaseCondaService { +module.exports = class CondaVersion extends BaseCondaService { static get category() { return 'version' } diff --git a/services/discourse/discourse.service.js b/services/discourse/discourse.service.js index 623e64faff324..96a8fa710e7f7 100644 --- a/services/discourse/discourse.service.js +++ b/services/discourse/discourse.service.js @@ -1,5 +1,6 @@ 'use strict' +const camelcase = require('camelcase') const Joi = require('joi') const { BaseJsonService } = require('..') const { metric } = require('../../lib/text-formatters') @@ -38,6 +39,12 @@ class DiscourseBase extends BaseJsonService { function DiscourseMetricIntegrationFactory({ metricName, property }) { return class DiscourseMetric extends DiscourseBase { + static get name() { + // The space is needed so we get 'DiscourseTopics' rather than + // 'Discoursetopics'. `camelcase()` removes it. + return camelcase(`Discourse ${metricName}`, { pascalCase: true }) + } + static get route() { return this.buildRoute(metricName) } diff --git a/services/dub/dub-download.service.js b/services/dub/dub-download.service.js index 70a6eec3a0b00..0cfa3ddb19610 100644 --- a/services/dub/dub-download.service.js +++ b/services/dub/dub-download.service.js @@ -16,26 +16,34 @@ const schema = Joi.object({ }) function DownloadsForInterval(interval) { - const { base, messageSuffix } = { + const { base, messageSuffix, name } = { daily: { base: 'dub/dd', messageSuffix: '/day', + name: 'DubDownloadsDay', }, weekly: { base: 'dub/dw', messageSuffix: '/week', + name: 'DubDownloadsWeek', }, monthly: { base: 'dub/dm', messageSuffix: '/month', + name: 'DubDownloadsMonth', }, total: { base: 'dub/dt', messageSuffix: '', + name: 'DubDownloadsTotal', }, }[interval] return class DubDownloads extends BaseJsonService { + static get name() { + return name + } + static render({ downloads, version }) { const label = version ? `downloads@${version}` : 'downloads' return { diff --git a/services/eclipse-marketplace/eclipse-marketplace-downloads.service.js b/services/eclipse-marketplace/eclipse-marketplace-downloads.service.js index 3c123c1844752..991441d4f87c6 100644 --- a/services/eclipse-marketplace/eclipse-marketplace-downloads.service.js +++ b/services/eclipse-marketplace/eclipse-marketplace-downloads.service.js @@ -25,19 +25,25 @@ const totalResponseSchema = Joi.object({ }).required() function DownloadsForInterval(interval) { - const { base, schema, messageSuffix = '' } = { + const { base, schema, messageSuffix = '', name } = { month: { base: 'eclipse-marketplace/dm', messageSuffix: '/month', schema: monthlyResponseSchema, + name: 'EclipseMarketplaceDownloadsMonth', }, total: { base: 'eclipse-marketplace/dt', schema: totalResponseSchema, + name: 'EclipseMarketplaceDownloadsTotal', }, }[interval] return class EclipseMarketplaceDownloads extends EclipseMarketplaceBase { + static get name() { + return name + } + static get category() { return 'downloads' } diff --git a/services/gratipay/gratipay.service.js b/services/gratipay/gratipay.service.js index 3551dd039aae4..eca49ae023c55 100644 --- a/services/gratipay/gratipay.service.js +++ b/services/gratipay/gratipay.service.js @@ -2,11 +2,25 @@ const { deprecatedService } = require('..') -module.exports = deprecatedService({ +const commonAttrs = { category: 'funding', - route: { - format: '(?:gittip|gratipay(?:/user|/team|/project)?)/(?:.*)', - }, label: 'gratipay', dateAdded: new Date('2017-12-29'), -}) +} + +module.exports = [ + deprecatedService({ + route: { + base: 'gittip', + format: '(?:/user|/team|/project)?/(?:.*)', + }, + ...commonAttrs, + }), + deprecatedService({ + route: { + base: 'gratipay', + format: '(?:/user|/team|/project)?/(?:.*)', + }, + ...commonAttrs, + }), +] diff --git a/services/hexpm/hexpm.service.js b/services/hexpm/hexpm.service.js index 80c94618aa452..418d6c31d9ae3 100644 --- a/services/hexpm/hexpm.service.js +++ b/services/hexpm/hexpm.service.js @@ -122,22 +122,29 @@ class HexPmVersion extends BaseHexPmService { } function DownloadsForInterval(interval) { - const { base, messageSuffix } = { + const { base, messageSuffix, name } = { day: { base: 'hexpm/dd', messageSuffix: '/day', + name: 'HexPmDownloadsDay', }, week: { base: 'hexpm/dw', messageSuffix: '/week', + name: 'HexPmDownloadsWeek', }, all: { base: 'hexpm/dt', messageSuffix: '', + name: 'HexPmDownloadsTotal', }, }[interval] return class HexPmDownloads extends BaseHexPmService { + static get name() { + return name + } + static render({ downloads }) { return { message: `${metric(downloads)}${messageSuffix}`, diff --git a/services/jetbrains/jetbrains-version.service.js b/services/jetbrains/jetbrains-version.service.js index 15c859b746752..ce3643024edcf 100644 --- a/services/jetbrains/jetbrains-version.service.js +++ b/services/jetbrains/jetbrains-version.service.js @@ -20,7 +20,7 @@ const schema = Joi.object({ }).required(), }).required() -module.exports = class JetbrainsDownloads extends JetbrainsBase { +module.exports = class JetbrainsVersion extends JetbrainsBase { static get category() { return 'version' } diff --git a/services/npm/npm-downloads.service.js b/services/npm/npm-downloads.service.js index ef9b525b2f746..a4283fc803a44 100644 --- a/services/npm/npm-downloads.service.js +++ b/services/npm/npm-downloads.service.js @@ -18,26 +18,30 @@ const rangeResponseSchema = Joi.object({ }).required() function DownloadsForInterval(interval) { - const { base, messageSuffix = '', query, isRange = false } = { + const { base, messageSuffix = '', query, isRange = false, name } = { week: { base: 'npm/dw', messageSuffix: '/w', query: 'point/last-week', + name: 'NpmDownloadsWeek', }, month: { base: 'npm/dm', messageSuffix: '/m', query: 'point/last-month', + name: 'NpmDownloadsMonth', }, year: { base: 'npm/dy', messageSuffix: '/y', query: 'point/last-year', + name: 'NpmDownloadsYear', }, total: { base: 'npm/dt', query: 'range/1000-01-01:3000-01-01', isRange: true, + name: 'NpmDownloadsTotal', }, }[interval] @@ -46,6 +50,10 @@ function DownloadsForInterval(interval) { // This hits an entirely different API from the rest of the NPM services, so // it does not use NpmBase. return class NpmDownloads extends BaseJsonService { + static get name() { + return name + } + static get category() { return 'downloads' } diff --git a/services/nuget/nuget-v2-service-family.js b/services/nuget/nuget-v2-service-family.js index 1d7704c193426..102272f5623f7 100644 --- a/services/nuget/nuget-v2-service-family.js +++ b/services/nuget/nuget-v2-service-family.js @@ -96,11 +96,12 @@ async function fetch( * apiBaseUrl: The complete base URL of the API, e.g. https://api.example.com/api/v2 */ function createServiceFamily({ + title, + name = title, defaultLabel, serviceBaseUrl, apiBaseUrl, odataFormat, - title, examplePackageName, exampleVersion, examplePrereleaseVersion, @@ -116,6 +117,10 @@ function createServiceFamily({ } class NugetVersionService extends Base { + static get name() { + return `${name}Version` + } + static get category() { return 'version' } @@ -169,6 +174,10 @@ function createServiceFamily({ } class NugetDownloadService extends Base { + static get name() { + return `${name}Downloads` + } + static get category() { return 'downloads' } diff --git a/services/packagecontrol/packagecontrol.service.js b/services/packagecontrol/packagecontrol.service.js index 335c7e5b86c64..304f0b0544b73 100644 --- a/services/packagecontrol/packagecontrol.service.js +++ b/services/packagecontrol/packagecontrol.service.js @@ -26,7 +26,7 @@ const schema = Joi.object({ }) function DownloadsForInterval(interval) { - const { base, messageSuffix, transform } = { + const { base, messageSuffix, transform, name } = { day: { base: 'packagecontrol/dd', messageSuffix: '/day', @@ -39,6 +39,7 @@ function DownloadsForInterval(interval) { }) return downloads }, + name: 'PackageControlDownloadsDay', }, week: { base: 'packagecontrol/dw', @@ -54,6 +55,7 @@ function DownloadsForInterval(interval) { }) return downloads }, + name: 'PackageControlDownloadsWeek', }, month: { base: 'packagecontrol/dm', @@ -69,15 +71,21 @@ function DownloadsForInterval(interval) { }) return downloads }, + name: 'PackageControlDownloadsMonth', }, total: { base: 'packagecontrol/dt', messageSuffix: '', transform: resp => resp.installs.total, + name: 'PackageControlDownloadsTotal', }, }[interval] return class PackageControlDownloads extends BaseJsonService { + static get name() { + return name + } + static render({ downloads }) { return { message: `${metric(downloads)}${messageSuffix}`, diff --git a/services/powershellgallery/powershellgallery.service.js b/services/powershellgallery/powershellgallery.service.js index d053bd6bf74f5..e92df73f7dc00 100644 --- a/services/powershellgallery/powershellgallery.service.js +++ b/services/powershellgallery/powershellgallery.service.js @@ -16,6 +16,7 @@ const { NugetVersionService: PowershellGalleryVersion, NugetDownloadService: PowershellGalleryDownloads, } = createServiceFamily({ + name: 'PowershellGallery', defaultLabel: 'powershell gallery', serviceBaseUrl: 'powershellgallery', apiBaseUrl, diff --git a/services/puppetforge/puppetforge-modules.service.js b/services/puppetforge/puppetforge-modules.service.js index a159d97f36777..f49c7ce93e291 100644 --- a/services/puppetforge/puppetforge-modules.service.js +++ b/services/puppetforge/puppetforge-modules.service.js @@ -183,6 +183,13 @@ class PuppetforgeModules extends LegacyService { return 'other' } + static get route() { + return { + base: 'puppetforge', + pattern: '([^/]+)/([^/]+)/([^/]+)', + } + } + static registerLegacyRouteHandler({ camp, cache }) { camp.route( /^\/puppetforge\/([^/]+)\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, diff --git a/services/puppetforge/puppetforge-users.service.js b/services/puppetforge/puppetforge-users.service.js index 9cd6314d31f68..0b2ecb346f194 100644 --- a/services/puppetforge/puppetforge-users.service.js +++ b/services/puppetforge/puppetforge-users.service.js @@ -81,6 +81,13 @@ class PuppetforgeUsers extends LegacyService { return 'other' } + static get route() { + return { + base: 'puppetforge', + pattern: '([^/]+)/([^/]+)/([^/]+)', + } + } + static registerLegacyRouteHandler({ camp, cache }) { camp.route( /^\/puppetforge\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, diff --git a/services/resharper/resharper.service.js b/services/resharper/resharper.service.js index 86fb7a96f0783..60585f7464721 100644 --- a/services/resharper/resharper.service.js +++ b/services/resharper/resharper.service.js @@ -3,6 +3,7 @@ const { createServiceFamily } = require('../nuget/nuget-v2-service-family') module.exports = createServiceFamily({ + name: 'ResharperPlugin', defaultLabel: 'resharper', serviceBaseUrl: 'resharper', apiBaseUrl: 'https://resharper-plugins.jetbrains.com/api/v2', diff --git a/services/snap-ci/snap-ci.service.js b/services/snap-ci/snap-ci.service.js index 6d6fdf1a8c339..73d03fe920c5f 100644 --- a/services/snap-ci/snap-ci.service.js +++ b/services/snap-ci/snap-ci.service.js @@ -2,11 +2,25 @@ const { deprecatedService } = require('..') -module.exports = deprecatedService({ +const commonAttrs = { category: 'build', - route: { - format: 'snap(?:-ci?)/(?:[^/]+/[^/]+)(?:/(?:.+))', - }, label: 'snap ci', dateAdded: new Date('2018-01-23'), -}) +} + +module.exports = [ + deprecatedService({ + route: { + base: 'snap', + format: '(?:[^/]+/[^/]+)(?:/(?:.+))', + }, + ...commonAttrs, + }), + deprecatedService({ + route: { + base: 'snap-ci', + format: '(?:[^/]+/[^/]+)(?:/(?:.+))', + }, + ...commonAttrs, + }), +] diff --git a/services/static-badge/query-string-static.service.js b/services/static-badge/query-string-static.service.js index ac084da3fb81d..635fb59f3f64d 100644 --- a/services/static-badge/query-string-static.service.js +++ b/services/static-badge/query-string-static.service.js @@ -14,6 +14,7 @@ module.exports = class QueryStringStaticBadge extends BaseStaticService { static get route() { return { + base: '', pattern: 'static/:schemaVersion(v1)', // All but one of the parameters are parsed via coalesceBadge. This // reuses what is the override behaviour for other badges. diff --git a/services/static-badge/static-badge.service.js b/services/static-badge/static-badge.service.js index 7d4070b393fa8..1a86808d38fb1 100644 --- a/services/static-badge/static-badge.service.js +++ b/services/static-badge/static-badge.service.js @@ -10,6 +10,7 @@ module.exports = class StaticBadge extends BaseStaticService { static get route() { return { + base: '', format: '(?::|badge/)((?:[^-]|--)*?)-?((?:[^-]|--)*)-((?:[^-]|--)+)', capture: ['label', 'message', 'color'], } diff --git a/services/vaadin-directory/vaadin-directory.service.js b/services/vaadin-directory/vaadin-directory.service.js index b63878f0f0091..94855a7129e07 100644 --- a/services/vaadin-directory/vaadin-directory.service.js +++ b/services/vaadin-directory/vaadin-directory.service.js @@ -166,6 +166,14 @@ class VaadinDirectory extends LegacyService { return 'other' } + static get route() { + return { + base: 'vaadin-directory', + pattern: + ':which(star|stars|status|rating|rc|rating-count|v|version|rd|release-date)/:urlIdentifier', + } + } + static registerLegacyRouteHandler({ camp, cache }) { camp.route( /^\/vaadin-directory\/(star|stars|status|rating|rc|rating-count|v|version|rd|release-date)\/(.*).(svg|png|gif|jpg|json)$/, diff --git a/services/wordpress/wordpress-downloads.service.js b/services/wordpress/wordpress-downloads.service.js index 20063f356fa6b..459b77a1d658d 100644 --- a/services/wordpress/wordpress-downloads.service.js +++ b/services/wordpress/wordpress-downloads.service.js @@ -25,6 +25,10 @@ function DownloadsForExtensionType(extensionType) { const { capt, exampleSlug } = extensionData[extensionType] return class WordpressDownloads extends BaseWordpress { + static get name() { + return `Wordpress${capt}Downloads` + } + static render({ response }) { return { message: metric(response.downloaded), @@ -66,6 +70,10 @@ function InstallsForExtensionType(extensionType) { const { capt, exampleSlug } = extensionData[extensionType] return class WordpressInstalls extends BaseWordpress { + static get name() { + return `Wordpress${capt}Installs` + } + static get extensionType() { return extensionType } @@ -105,30 +113,38 @@ function InstallsForExtensionType(extensionType) { } function DownloadsForInterval(interval) { - const { base, messageSuffix = '', query } = { + const { base, messageSuffix = '', query, name } = { day: { base: 'wordpress/plugin/dd', messageSuffix: '/day', query: 1, + name: 'WordpressDownloadsDay', }, week: { base: 'wordpress/plugin/dw', messageSuffix: '/week', query: 7, + name: 'WordpressDownloadsWeek', }, month: { base: 'wordpress/plugin/dm', messageSuffix: '/month', query: 30, + name: 'WordpressDownloadsMonth', }, year: { base: 'wordpress/plugin/dy', messageSuffix: '/year', query: 365, + name: 'WordpressDownloadsYear', }, }[interval] return class WordpressDownloads extends BaseJsonService { + static get name() { + return name + } + static get category() { return 'downloads' } diff --git a/services/wordpress/wordpress-rating.service.js b/services/wordpress/wordpress-rating.service.js index d160ab95fffcc..dcabd1d914188 100644 --- a/services/wordpress/wordpress-rating.service.js +++ b/services/wordpress/wordpress-rating.service.js @@ -38,6 +38,10 @@ function RatingForExtensionType(extensionType) { const { capt, exampleSlug } = extensionData[extensionType] return class WordpressRating extends WordpressRatingBase { + static get name() { + return `Wordpress${capt}Rating` + } + static get route() { return { base: `wordpress/${extensionType}/rating`, @@ -70,6 +74,10 @@ function StarsForExtensionType(extensionType) { const { capt, exampleSlug } = extensionData[extensionType] return class WordpressStars extends WordpressRatingBase { + static get name() { + return `Wordpress${capt}Stars` + } + static render({ response }) { const rating = (response.rating / 100) * 5 return { message: starRating(rating), color: floorCount(rating, 2, 3, 4) } diff --git a/services/wordpress/wordpress-version.service.js b/services/wordpress/wordpress-version.service.js index 541871e9738cc..259cad3f13bda 100644 --- a/services/wordpress/wordpress-version.service.js +++ b/services/wordpress/wordpress-version.service.js @@ -16,7 +16,11 @@ function VersionForExtensionType(extensionType) { }, }[extensionType] - return class WordpressPluginVersion extends BaseWordpress { + return class WordpressVersion extends BaseWordpress { + static get name() { + return `Wordpress${capt}Version` + } + static get extensionType() { return extensionType } diff --git a/test-fixtures/valid-array.fixture.js b/test-fixtures/valid-array.fixture.js index 0d96853969df0..4c4cec06a4da0 100644 --- a/test-fixtures/valid-array.fixture.js +++ b/test-fixtures/valid-array.fixture.js @@ -9,7 +9,10 @@ class GoodServiceOne extends BaseJsonService { } static get route() { - return { pattern: 'good/one' } + return { + base: 'good', + pattern: 'one', + } } } class GoodServiceTwo extends LegacyService { @@ -18,7 +21,10 @@ class GoodServiceTwo extends LegacyService { } static get route() { - return { pattern: 'good/two' } + return { + base: 'good', + pattern: 'two', + } } } diff --git a/test-fixtures/valid-class.fixture.js b/test-fixtures/valid-class.fixture.js index 248234ee8599b..f192c498169f6 100644 --- a/test-fixtures/valid-class.fixture.js +++ b/test-fixtures/valid-class.fixture.js @@ -8,7 +8,10 @@ class GoodService extends BaseJsonService { } static get route() { - return { pattern: 'good' } + return { + base: 'it/is', + pattern: 'good', + } } } diff --git a/test-fixtures/valid-object.fixture.js b/test-fixtures/valid-object.fixture.js index b022fa1d22ea6..6a0faeaadb943 100644 --- a/test-fixtures/valid-object.fixture.js +++ b/test-fixtures/valid-object.fixture.js @@ -9,7 +9,10 @@ class GoodServiceOne extends BaseJsonService { } static get route() { - return { pattern: 'good/one' } + return { + base: 'good', + pattern: 'one', + } } } class GoodServiceTwo extends LegacyService { @@ -18,7 +21,10 @@ class GoodServiceTwo extends LegacyService { } static get route() { - return { pattern: 'good/two' } + return { + base: 'good', + pattern: 'two', + } } }