diff --git a/core/base-service/base.js b/core/base-service/base.js index 96efddbf920e2b31a5541290bf9bf0f12bd331c8..4f6d3da09e746ff737767ec1087b5ac8acc76f45 100644 --- a/core/base-service/base.js +++ b/core/base-service/base.js @@ -21,6 +21,7 @@ import { } from './errors.js' import { validateExample, transformExample } from './examples.js' import { fetch } from './got.js' +import { getEnum } from './openapi.js' import { makeFullUrl, assertValidRoute, @@ -102,6 +103,26 @@ class BaseService { throw new Error(`Route not defined for ${this.name}`) } + /** + * Extract an array of allowed values from this service's route pattern + * for a given route parameter + * + * @param {string} param The name of a param in this service's route pattern + * @returns {string[]} Array of allowed values for this param + */ + static getEnum(param) { + if (!('pattern' in this.route)) { + throw new Error('getEnum() requires route to have a .pattern property') + } + const enumeration = getEnum(this.route.pattern, param) + if (!Array.isArray(enumeration)) { + throw new Error( + `Could not extract enum for param ${param} from pattern ${this.route.pattern}`, + ) + } + return enumeration + } + /** * Configuration for the authentication helper that prepares credentials * for upstream requests. diff --git a/core/base-service/base.spec.js b/core/base-service/base.spec.js index 8f63654679bec8c00a3b7f5be45d28e650a929b8..dbd096e600bc255553cebd7988dcf525ba944cb5 100644 --- a/core/base-service/base.spec.js +++ b/core/base-service/base.spec.js @@ -539,6 +539,7 @@ describe('BaseService', function () { ).to.not.contain('service_response_bytes_bucket') }) }) + describe('auth', function () { class AuthService extends DummyService { static auth = { @@ -592,4 +593,44 @@ describe('BaseService', function () { }) }) }) + + describe('getEnum', function () { + class EnumService extends DummyService { + static route = { + base: 'foo', + pattern: ':namedParamA/:namedParamB(this|that)', + queryParamSchema, + } + } + + it('returns an array of allowed values', async function () { + expect(EnumService.getEnum('namedParamB')).to.deep.equal(['this', 'that']) + }) + + it('throws if param name is invalid', async function () { + expect(() => EnumService.getEnum('notAValidParam')).to.throw( + 'Could not extract enum for param notAValidParam from pattern :namedParamA/:namedParamB(this|that)', + ) + }) + + it('throws if param name is not an enum', async function () { + expect(() => EnumService.getEnum('namedParamA')).to.throw( + 'Could not extract enum for param namedParamA from pattern :namedParamA/:namedParamB(this|that)', + ) + }) + + it('throws if route does not have a pattern', async function () { + class FormatService extends DummyService { + static route = { + base: 'foo', + format: '([^/]+?)', + queryParamSchema, + } + } + + expect(() => FormatService.getEnum('notAValidParam')).to.throw( + 'getEnum() requires route to have a .pattern property', + ) + }) + }) }) diff --git a/core/base-service/openapi.js b/core/base-service/openapi.js index 92847b71f52ae6d20caad7410d598686877afd9d..a5399a328cdce01011f7ae9ac17f32ab10669bf2 100644 --- a/core/base-service/openapi.js +++ b/core/base-service/openapi.js @@ -466,4 +466,11 @@ function queryParams(...params) { * @property {boolean} allowEmptyValue If true, allows the ability to pass an empty value to this parameter */ -export { category2openapi, pathParam, pathParams, queryParam, queryParams } +export { + category2openapi, + getEnum, + pathParam, + pathParams, + queryParam, + queryParams, +} diff --git a/services/codefactor/codefactor-grade.service.js b/services/codefactor/codefactor-grade.service.js index 0752654565fcac5c7fa9fd86f4b7ee4375113b2d..26ed28208b7bc140e84ace59521d5e996b0fc9d3 100644 --- a/services/codefactor/codefactor-grade.service.js +++ b/services/codefactor/codefactor-grade.service.js @@ -1,5 +1,5 @@ import Joi from 'joi' -import { BaseSvgScrapingService } from '../index.js' +import { BaseSvgScrapingService, pathParams } from '../index.js' import { isValidGrade, gradeColor } from './codefactor-helpers.js' const schema = Joi.object({ @@ -13,18 +13,52 @@ export default class CodeFactorGrade extends BaseSvgScrapingService { pattern: ':vcsType(github|bitbucket)/:user/:repo/:branch*', } - static examples = [ - { - title: 'CodeFactor Grade', - namedParams: { - vcsType: 'github', - user: 'microsoft', - repo: 'powertoys', - branch: 'main', + static openApi = { + '/codefactor/grade/{vcsType}/{user}/{repo}/{branch}': { + get: { + summary: 'CodeFactor Grade (with branch)', + parameters: pathParams( + { + name: 'vcsType', + example: 'github', + schema: { type: 'string', enum: this.getEnum('vcsType') }, + }, + { + name: 'user', + example: 'microsoft', + }, + { + name: 'repo', + example: 'powertoys', + }, + { + name: 'branch', + example: 'main', + }, + ), }, - staticPreview: this.render({ grade: 'B+' }), }, - ] + '/codefactor/grade/{vcsType}/{user}/{repo}': { + get: { + summary: 'CodeFactor Grade', + parameters: pathParams( + { + name: 'vcsType', + example: 'github', + schema: { type: 'string', enum: this.getEnum('vcsType') }, + }, + { + name: 'user', + example: 'microsoft', + }, + { + name: 'repo', + example: 'powertoys', + }, + ), + }, + }, + } static defaultBadgeData = { label: 'code quality' } diff --git a/services/conda/conda-downloads.service.js b/services/conda/conda-downloads.service.js index a08152ed42729cd0c52e0797ed4362ba18ee79e0..85b6e6b897aea08bc19f9274d685de2d0181896d 100644 --- a/services/conda/conda-downloads.service.js +++ b/services/conda/conda-downloads.service.js @@ -1,3 +1,4 @@ +import { pathParams } from '../index.js' import { renderDownloadsBadge } from '../downloads.js' import BaseCondaService from './conda-base.js' @@ -5,14 +6,28 @@ export default class CondaDownloads extends BaseCondaService { static category = 'downloads' static route = { base: 'conda', pattern: ':variant(d|dn)/:channel/:pkg' } - static examples = [ - { - title: 'Conda', - namedParams: { channel: 'conda-forge', package: 'python' }, - pattern: 'dn/:channel/:package', - staticPreview: this.render({ variant: 'dn', downloads: 5000000 }), + static openApi = { + '/conda/{variant}/{channel}/{package}': { + get: { + summary: 'Conda Downloads', + parameters: pathParams( + { + name: 'variant', + example: 'dn', + schema: { type: 'string', enum: this.getEnum('variant') }, + }, + { + name: 'channel', + example: 'conda-forge', + }, + { + name: 'package', + example: 'python', + }, + ), + }, }, - ] + } static render({ variant, downloads }) { const labelOverride = variant === 'dn' ? 'downloads' : 'conda|downloads' diff --git a/services/conda/conda-platform.service.js b/services/conda/conda-platform.service.js index fdf7cc13aa322cc9b6d9dc54fca9540232c05f9c..e16a8399a4be3b89a3869a20a6518ad7d4815535 100644 --- a/services/conda/conda-platform.service.js +++ b/services/conda/conda-platform.service.js @@ -1,20 +1,32 @@ +import { pathParams } from '../index.js' import BaseCondaService from './conda-base.js' export default class CondaPlatform extends BaseCondaService { static category = 'platform-support' static route = { base: 'conda', pattern: ':variant(p|pn)/:channel/:pkg' } - static examples = [ - { - title: 'Conda', - namedParams: { channel: 'conda-forge', package: 'python' }, - pattern: 'pn/:channel/:package', - staticPreview: this.render({ - variant: 'pn', - platforms: ['linux-64', 'win-32', 'osx-64', 'win-64'], - }), + static openApi = { + '/conda/{variant}/{channel}/{package}': { + get: { + summary: 'Conda Platform', + parameters: pathParams( + { + name: 'variant', + example: 'pn', + schema: { type: 'string', enum: this.getEnum('variant') }, + }, + { + name: 'channel', + example: 'conda-forge', + }, + { + name: 'package', + example: 'python', + }, + ), + }, }, - ] + } static render({ variant, platforms }) { return { diff --git a/services/depfu/depfu.service.js b/services/depfu/depfu.service.js index fd26a9e4297db719cb4e122ffb087d70a6342970..a1c0dbc2eec938b9b3585024fe642a701123cf6c 100644 --- a/services/depfu/depfu.service.js +++ b/services/depfu/depfu.service.js @@ -1,5 +1,10 @@ import Joi from 'joi' -import { BaseJsonService, InvalidParameter, redirector } from '../index.js' +import { + BaseJsonService, + InvalidParameter, + redirector, + pathParams, +} from '../index.js' const depfuSchema = Joi.object({ text: Joi.string().required(), @@ -13,16 +18,24 @@ class Depfu extends BaseJsonService { pattern: ':vcsType(github|gitlab)/:project+', } - static examples = [ - { - title: 'Depfu', - namedParams: { vcsType: 'github', project: 'depfu/example-ruby' }, - staticPreview: this.render({ - text: 'recent', - colorscheme: 'brightgreen', - }), + static openApi = { + '/depfu/dependencies/{vcsType}/{project}': { + get: { + summary: 'Depfu', + parameters: pathParams( + { + name: 'vcsType', + example: 'github', + schema: { type: 'string', enum: this.getEnum('vcsType') }, + }, + { + name: 'project', + example: 'depfu/example-ruby', + }, + ), + }, }, - ] + } static defaultBadgeData = { label: 'dependencies' } diff --git a/services/github/github-contributors.service.js b/services/github/github-contributors.service.js index 72162af27a79fe48c86d39e8ae58560b40c369e9..23d07dfe48c773b77b3457238ba5bbaf3e06eae8 100644 --- a/services/github/github-contributors.service.js +++ b/services/github/github-contributors.service.js @@ -1,5 +1,6 @@ import Joi from 'joi' import parseLinkHeader from 'parse-link-header' +import { pathParams } from '../index.js' import { renderContributorBadge } from '../contributor-count.js' import { GithubAuthV3Service } from './github-auth-service.js' import { documentation, httpErrorsFor } from './github-helpers.js' @@ -14,18 +15,29 @@ export default class GithubContributors extends GithubAuthV3Service { pattern: ':variant(contributors|contributors-anon)/:user/:repo', } - static examples = [ - { - title: 'GitHub contributors', - namedParams: { - variant: 'contributors', - user: 'cdnjs', - repo: 'cdnjs', + static openApi = { + '/github/{variant}/{user}/{repo}': { + get: { + summary: 'GitHub contributors', + description: documentation, + parameters: pathParams( + { + name: 'variant', + example: 'contributors', + schema: { type: 'string', enum: this.getEnum('variant') }, + }, + { + name: 'user', + example: 'cdnjs', + }, + { + name: 'repo', + example: 'cdnjs', + }, + ), }, - staticPreview: this.render({ contributorCount: 397 }), - documentation, }, - ] + } static defaultBadgeData = { label: 'contributors' } diff --git a/services/github/github-issue-detail.service.js b/services/github/github-issue-detail.service.js index c391c15bb86e32279414c3bd69c691ce5fdb8b40..f6fb5039ef4b078a303429316137c4a8041c7e4f 100644 --- a/services/github/github-issue-detail.service.js +++ b/services/github/github-issue-detail.service.js @@ -2,7 +2,7 @@ import Joi from 'joi' import { nonNegativeInteger } from '../validators.js' import { formatDate, metric } from '../text-formatters.js' import { age } from '../color-formatters.js' -import { InvalidResponse } from '../index.js' +import { InvalidResponse, pathParams } from '../index.js' import { GithubAuthV3Service } from './github-auth-service.js' import { documentation, @@ -179,35 +179,38 @@ export default class GithubIssueDetail extends GithubAuthV3Service { ':issueKind(issues|pulls)/detail/:property(state|title|author|label|comments|age|last-update|milestone)/:user/:repo/:number([0-9]+)', } - static examples = [ - { - title: 'GitHub issue/pull request detail', - namedParams: { - issueKind: 'issues', - property: 'state', - user: 'badges', - repo: 'shields', - number: '979', + static openApi = { + '/github/{issueKind}/detail/{property}/{user}/{repo}/{number}': { + get: { + summary: 'GitHub issue/pull request detail', + description: documentation, + parameters: pathParams( + { + name: 'issueKind', + example: 'issues', + schema: { type: 'string', enum: this.getEnum('issueKind') }, + }, + { + name: 'property', + example: 'state', + schema: { type: 'string', enum: this.getEnum('property') }, + }, + { + name: 'user', + example: 'badges', + }, + { + name: 'repo', + example: 'shields', + }, + { + name: 'number', + example: '979', + }, + ), }, - staticPreview: this.render({ - property: 'state', - value: { state: 'closed' }, - isPR: false, - number: '979', - }), - keywords: [ - 'state', - 'title', - 'author', - 'label', - 'comments', - 'age', - 'last update', - 'milestone', - ], - documentation, }, - ] + } static defaultBadgeData = { label: 'issue/pull request', diff --git a/services/github/github-milestone-detail.service.js b/services/github/github-milestone-detail.service.js index fe37ffb1f89fc41570a2c89c75352505879f8c3d..98be914dadfb02a86d7152de0a22dd2aeda8c10f 100644 --- a/services/github/github-milestone-detail.service.js +++ b/services/github/github-milestone-detail.service.js @@ -1,4 +1,5 @@ import Joi from 'joi' +import { pathParams } from '../index.js' import { metric } from '../text-formatters.js' import { nonNegativeInteger } from '../validators.js' import { GithubAuthV3Service } from './github-auth-service.js' @@ -18,23 +19,33 @@ export default class GithubMilestoneDetail extends GithubAuthV3Service { ':variant(issues-closed|issues-open|issues-total|progress|progress-percent)/:user/:repo/:number([0-9]+)', } - static examples = [ - { - title: 'GitHub milestone', - namedParams: { - variant: 'issues-open', - user: 'badges', - repo: 'shields', - number: '1', + static openApi = { + '/github/milestones/{variant}/{user}/{repo}/{number}': { + get: { + summary: 'GitHub milestone details', + description: documentation, + parameters: pathParams( + { + name: 'variant', + example: 'issues-open', + schema: { type: 'string', enum: this.getEnum('variant') }, + }, + { + name: 'user', + example: 'badges', + }, + { + name: 'repo', + example: 'shields', + }, + { + name: 'number', + example: '1', + }, + ), }, - staticPreview: { - label: 'milestone issues', - message: '17/22', - color: 'blue', - }, - documentation, }, - ] + } static defaultBadgeData = { label: 'milestones', color: 'informational' } diff --git a/services/github/github-milestone.service.js b/services/github/github-milestone.service.js index 973bb6649eae493016e2613900de3a646b8ba461..458071dcf023adcd72d0eef923ae0c96f7eef4f4 100644 --- a/services/github/github-milestone.service.js +++ b/services/github/github-milestone.service.js @@ -1,4 +1,5 @@ import Joi from 'joi' +import { pathParams } from '../index.js' import { metric } from '../text-formatters.js' import { GithubAuthV3Service } from './github-auth-service.js' import { documentation, httpErrorsFor } from './github-helpers.js' @@ -18,22 +19,29 @@ export default class GithubMilestone extends GithubAuthV3Service { pattern: ':variant(open|closed|all)/:user/:repo', } - static examples = [ - { - title: 'GitHub milestones', - namedParams: { - user: 'badges', - repo: 'shields', - variant: 'open', + static openApi = { + '/github/milestones/{variant}/{user}/{repo}': { + get: { + summary: 'GitHub number of milestones', + description: documentation, + parameters: pathParams( + { + name: 'variant', + example: 'open', + schema: { type: 'string', enum: this.getEnum('variant') }, + }, + { + name: 'user', + example: 'badges', + }, + { + name: 'repo', + example: 'shields', + }, + ), }, - staticPreview: { - label: 'milestones', - message: '2', - color: 'red', - }, - documentation, }, - ] + } static defaultBadgeData = { label: 'milestones', diff --git a/services/homebrew/homebrew-downloads.service.js b/services/homebrew/homebrew-downloads.service.js index e7989e48b362a4122969441f66f6a14f8bdff620..6dfa10d1a3d4ae1e78e993979a3e1cb6918e8357 100644 --- a/services/homebrew/homebrew-downloads.service.js +++ b/services/homebrew/homebrew-downloads.service.js @@ -1,6 +1,6 @@ import Joi from 'joi' import { renderDownloadsBadge } from '../downloads.js' -import { BaseJsonService } from '../index.js' +import { BaseJsonService, pathParams } from '../index.js' import { nonNegativeInteger } from '../validators.js' function getSchema({ formula }) { @@ -38,13 +38,24 @@ export default class HomebrewDownloads extends BaseJsonService { pattern: 'installs/:interval(dm|dq|dy)/:formula', } - static examples = [ - { - title: 'homebrew downloads', - namedParams: { interval: 'dm', formula: 'cake' }, - staticPreview: renderDownloadsBadge({ interval: 'month', downloads: 93 }), + static openApi = { + '/homebrew/installs/{interval}/{formula}': { + get: { + summary: 'homebrew downloads', + parameters: pathParams( + { + name: 'interval', + example: 'dm', + schema: { type: 'string', enum: this.getEnum('interval') }, + }, + { + name: 'formula', + example: 'cake', + }, + ), + }, }, - ] + } static defaultBadgeData = { label: 'downloads' } diff --git a/services/jsdelivr/jsdelivr-hits-github.service.js b/services/jsdelivr/jsdelivr-hits-github.service.js index 8d760b899266f3b4cf34b9d668f7dfe30619be9e..96ddc830f396f637fdc0ba519b0db0196c42ba91 100644 --- a/services/jsdelivr/jsdelivr-hits-github.service.js +++ b/services/jsdelivr/jsdelivr-hits-github.service.js @@ -1,3 +1,4 @@ +import { pathParams } from '../index.js' import { schema, periodMap, BaseJsDelivrService } from './jsdelivr-base.js' export default class JsDelivrHitsGitHub extends BaseJsDelivrService { @@ -6,17 +7,28 @@ export default class JsDelivrHitsGitHub extends BaseJsDelivrService { pattern: ':period(hd|hw|hm|hy)/:user/:repo', } - static examples = [ - { - title: 'jsDelivr hits (GitHub)', - namedParams: { - period: 'hm', - user: 'jquery', - repo: 'jquery', + static openApi = { + '/jsdelivr/gh/{period}/{user}/{repo}': { + get: { + summary: 'jsDelivr hits (GitHub)', + parameters: pathParams( + { + name: 'period', + example: 'hm', + schema: { type: 'string', enum: this.getEnum('period') }, + }, + { + name: 'user', + example: 'jquery', + }, + { + name: 'repo', + example: 'jquery', + }, + ), }, - staticPreview: this.render({ period: 'hm', hits: 9809876 }), }, - ] + } async fetch({ period, user, repo }) { return this._requestJson({ diff --git a/services/reddit/user-karma.service.js b/services/reddit/user-karma.service.js index 0ec007edc30badfeddf1f1500d70b664c51d0364..ca8b489d3451b076b6eeabbdd0d50cc78e447695 100644 --- a/services/reddit/user-karma.service.js +++ b/services/reddit/user-karma.service.js @@ -1,7 +1,7 @@ import Joi from 'joi' import { anyInteger } from '../validators.js' import { metric } from '../text-formatters.js' -import { BaseJsonService } from '../index.js' +import { BaseJsonService, pathParams } from '../index.js' const schema = Joi.object({ data: Joi.object({ @@ -18,18 +18,24 @@ export default class RedditUserKarma extends BaseJsonService { pattern: ':variant(link|comment|combined)/:user', } - static examples = [ - { - title: 'Reddit User Karma', - namedParams: { variant: 'combined', user: 'example' }, - staticPreview: { - label: 'combined karma', - message: 56, - color: 'brightgreen', - style: 'social', + static openApi = { + '/reddit/user-karma/{variant}/{user}': { + get: { + summary: 'Reddit User Karma', + parameters: pathParams( + { + name: 'variant', + example: 'combined', + schema: { type: 'string', enum: this.getEnum('variant') }, + }, + { + name: 'user', + example: 'example', + }, + ), }, }, - ] + } static _cacheLength = 7200 diff --git a/services/sourceforge/sourceforge-open-tickets.service.js b/services/sourceforge/sourceforge-open-tickets.service.js index d6fa6b2c26a2eb4899e69bab7511c80ef6ae8ea7..7c1cf76ae31a5ee65b8c5a5f114cfb955c4af362 100644 --- a/services/sourceforge/sourceforge-open-tickets.service.js +++ b/services/sourceforge/sourceforge-open-tickets.service.js @@ -1,7 +1,7 @@ import Joi from 'joi' import { metric } from '../text-formatters.js' import { nonNegativeInteger } from '../validators.js' -import { BaseJsonService } from '../index.js' +import { BaseJsonService, pathParams } from '../index.js' const schema = Joi.object({ count: nonNegativeInteger.required(), @@ -15,16 +15,24 @@ export default class SourceforgeOpenTickets extends BaseJsonService { pattern: ':project/:type(bugs|feature-requests)', } - static examples = [ - { - title: 'Sourceforge Open Tickets', - namedParams: { - type: 'bugs', - project: 'sevenzip', + static openApi = { + '/sourceforge/open-tickets/{project}/{type}': { + get: { + summary: 'Sourceforge Open Tickets', + parameters: pathParams( + { + name: 'project', + example: 'sevenzip', + }, + { + name: 'type', + example: 'bugs', + schema: { type: 'string', enum: this.getEnum('type') }, + }, + ), }, - staticPreview: this.render({ count: 1338 }), }, - ] + } static defaultBadgeData = { label: 'open tickets', diff --git a/services/testspace/testspace-test-count.service.js b/services/testspace/testspace-test-count.service.js index 09ec0e68142608995f32ab894482af140841e38a..aa670fc3be470671a6f3e311ded2ad08acb400b9 100644 --- a/services/testspace/testspace-test-count.service.js +++ b/services/testspace/testspace-test-count.service.js @@ -1,3 +1,4 @@ +import { pathParams } from '../index.js' import { metric as metricCount } from '../text-formatters.js' import TestspaceBase from './testspace-base.js' @@ -8,21 +9,32 @@ export default class TestspaceTestCount extends TestspaceBase { ':metric(total|passed|failed|skipped|errored|untested)/:org/:project/:space+', } - static examples = [ - { - title: 'Testspace tests', - namedParams: { - metric: 'passed', - org: 'swellaby', - project: 'swellaby:testspace-sample', - space: 'main', + static openApi = { + '/testspace/{metric}/{org}/{project}/{space}': { + get: { + summary: 'Testspace tests', + parameters: pathParams( + { + name: 'metric', + example: 'passed', + schema: { type: 'string', enum: this.getEnum('metric') }, + }, + { + name: 'org', + example: 'swellaby', + }, + { + name: 'project', + example: 'swellaby:testspace-sample', + }, + { + name: 'space', + example: 'main', + }, + ), }, - staticPreview: this.render({ - metric: 'passed', - value: 31, - }), }, - ] + } static render({ value, metric }) { let color = 'informational' diff --git a/services/vaadin-directory/vaadin-directory-rating.service.js b/services/vaadin-directory/vaadin-directory-rating.service.js index c07ddf091b09933ce1b029f6e3ed78b1f508eb87..846e7416558bd4273eb5c2058a300e90c840ef1a 100644 --- a/services/vaadin-directory/vaadin-directory-rating.service.js +++ b/services/vaadin-directory/vaadin-directory-rating.service.js @@ -1,3 +1,4 @@ +import { pathParams } from '../index.js' import { starRating } from '../text-formatters.js' import { floorCount as floorCountColor } from '../color-formatters.js' import { BaseVaadinDirectoryService } from './vaadin-directory-base.js' @@ -10,15 +11,24 @@ export default class VaadinDirectoryRating extends BaseVaadinDirectoryService { pattern: ':format(star|stars|rating)/:packageName', } - static examples = [ - { - title: 'Vaadin Directory', - pattern: ':format(stars|rating)/:packageName', - namedParams: { format: 'rating', packageName: 'vaadinvaadin-grid' }, - staticPreview: this.render({ format: 'rating', score: 4.75 }), - keywords: ['vaadin-directory'], + static openApi = { + '/vaadin-directory/{format}/{packageName}': { + get: { + summary: 'Vaadin Directory Rating', + parameters: pathParams( + { + name: 'format', + example: 'rating', + schema: { type: 'string', enum: this.getEnum('format') }, + }, + { + name: 'packageName', + example: 'vaadinvaadin-grid', + }, + ), + }, }, - ] + } static defaultBadgeData = { label: 'rating',