diff --git a/core/base-service/base.js b/core/base-service/base.js index 270c3e20867b7..96efddbf920e2 100644 --- a/core/base-service/base.js +++ b/core/base-service/base.js @@ -189,6 +189,22 @@ class BaseService { this.examples.forEach((example, index) => validateExample(example, index, this), ) + + // ensure openApi spec matches route + if (this.openApi) { + const preparedRoute = prepareRoute(this.route) + for (const [key, value] of Object.entries(this.openApi)) { + let example = key + for (const param of value.get.parameters) { + example = example.replace(`{${param.name}}`, param.example) + } + if (!example.match(preparedRoute.regex)) { + throw new Error( + `Inconsistent Open Api spec and Route found for service ${this.name}`, + ) + } + } + } } static getDefinition() { diff --git a/core/base-service/index.js b/core/base-service/index.js index f249090501724..5808b6211d56a 100644 --- a/core/base-service/index.js +++ b/core/base-service/index.js @@ -15,6 +15,7 @@ import { Deprecated, ImproperlyConfigured, } from './errors.js' +import { pathParam, pathParams, queryParam, queryParams } from './openapi.js' export { BaseService, @@ -32,4 +33,8 @@ export { InvalidParameter, ImproperlyConfigured, Deprecated, + pathParam, + pathParams, + queryParam, + queryParams, } diff --git a/core/base-service/loader.js b/core/base-service/loader.js index 7cd272e0d0500..9b72355436299 100644 --- a/core/base-service/loader.js +++ b/core/base-service/loader.js @@ -83,6 +83,18 @@ async function loadServiceClasses(servicePaths) { }, ) + const routeSummaries = [] + serviceClasses.forEach(function (serviceClass) { + if (serviceClass.openApi) { + for (const route of Object.values(serviceClass.openApi)) { + routeSummaries.push(route.get.summary) + } + } + }) + assertNamesUnique(routeSummaries, { + message: 'Duplicate route summary found', + }) + return serviceClasses } diff --git a/core/base-service/openapi.js b/core/base-service/openapi.js index c357e5aed7960..92847b71f52ae 100644 --- a/core/base-service/openapi.js +++ b/core/base-service/openapi.js @@ -1,3 +1,9 @@ +/** + * Functions for publishing the shields.io URL schema as an OpenAPI Document + * + * @module + */ + const baseUrl = process.env.BASE_URL const globalParamRefs = [ { $ref: '#/components/parameters/style' }, @@ -332,4 +338,132 @@ function category2openapi(category, services) { return spec } -export { category2openapi } +/** + * Helper function for assembling an OpenAPI path parameter object + * + * @param {module:core/base-service/openapi~PathParamInput} param Input param + * @returns {module:core/base-service/openapi~OpenApiParam} OpenAPI Parameter Object + * @see https://swagger.io/specification/#parameter-object + */ +function pathParam({ + name, + example, + schema = { type: 'string' }, + description, +}) { + return { name, in: 'path', required: true, schema, example, description } +} + +/** + * Helper function for assembling an array of OpenAPI path parameter objects + * The code + * ``` + * const params = pathParams( + * { name: 'name1', example: 'example1' }, + * { name: 'name2', example: 'example2' }, + * ) + * ``` + * is equivilent to + * ``` + * const params = [ + * pathParam({ name: 'name1', example: 'example1' }), + * pathParam({ name: 'name2', example: 'example2' }), + * ] + * ``` + * + * @param {...module:core/base-service/openapi~PathParamInput} params Input params + * @returns {Array.} Array of OpenAPI Parameter Objects + * @see {@link module:core/base-service/openapi~pathParam} + */ +function pathParams(...params) { + return params.map(param => pathParam(param)) +} + +/** + * Helper function for assembling an OpenAPI query parameter object + * + * @param {module:core/base-service/openapi~QueryParamInput} param Input param + * @returns {module:core/base-service/openapi~OpenApiParam} OpenAPI Parameter Object + * @see https://swagger.io/specification/#parameter-object + */ +function queryParam({ + name, + example, + schema = { type: 'string' }, + required = false, + description, +}) { + const param = { name, in: 'query', required, schema, example, description } + if (example === null && schema.type === 'boolean') { + param.allowEmptyValue = true + } + return param +} + +/** + * Helper function for assembling an array of OpenAPI query parameter objects + * The code + * ``` + * const params = queryParams( + * { name: 'name1', example: 'example1' }, + * { name: 'name2', example: 'example2' }, + * ) + * ``` + * is equivilent to + * ``` + * const params = [ + * queryParam({ name: 'name1', example: 'example1' }), + * queryParams({ name: 'name2', example: 'example2' }), + * ] + * ``` + * + * @param {...module:core/base-service/openapi~QueryParamInput} params Input params + * @returns {Array.} Array of OpenAPI Parameter Objects + * @see {@link module:core/base-service/openapi~queryParam} + */ +function queryParams(...params) { + return params.map(param => queryParam(param)) +} + +/** + * @typedef {object} PathParamInput + * @property {string} name The name of the parameter. Parameter names are case sensitive + * @property {string} example Example of a valid value for this parameter + * @property {object} [schema={ type: 'string' }] Parameter schema. + * An [OpenAPI Schema object](https://swagger.io/specification/#schema-object) + * specifying the parameter type. + * Normally this should be omitted as all path parameters are strings. + * Use this when we also want to pass an enum of valid parameters + * to be presented as a drop-down in the frontend. e.g: + * `{'type': 'string', 'enum': ['github', 'bitbucket'}` (Optional) + * @property {string} description A brief description of the parameter (Optional) + */ + +/** + * @typedef {object} QueryParamInput + * @property {string} name The name of the parameter. Parameter names are case sensitive + * @property {string|null} example Example of a valid value for this parameter + * @property {object} [schema={ type: 'string' }] Parameter schema. + * An [OpenAPI Schema object](https://swagger.io/specification/#schema-object) + * specifying the parameter type. This can normally be omitted. + * Query params are usually strings. (Optional) + * @property {boolean} [required=false] Determines whether this parameter is mandatory (Optional) + * @property {string} description A brief description of the parameter (Optional) + */ + +/** + * OpenAPI Parameter Object + * + * @typedef {object} OpenApiParam + * @property {string} name The name of the parameter + * @property {string|null} example Example of a valid value for this parameter + * @property {('path'|'query')} in The location of the parameter + * @property {object} schema Parameter schema. + * An [OpenAPI Schema object](https://swagger.io/specification/#schema-object) + * specifying the parameter type. + * @property {boolean} required Determines whether this parameter is mandatory + * @property {string} description A brief description of the parameter + * @property {boolean} allowEmptyValue If true, allows the ability to pass an empty value to this parameter + */ + +export { category2openapi, pathParam, pathParams, queryParam, queryParams } diff --git a/core/base-service/openapi.spec.js b/core/base-service/openapi.spec.js index a70d803716df1..0577f078e682c 100644 --- a/core/base-service/openapi.spec.js +++ b/core/base-service/openapi.spec.js @@ -1,5 +1,11 @@ import chai from 'chai' -import { category2openapi } from './openapi.js' +import { + category2openapi, + pathParam, + pathParams, + queryParam, + queryParams, +} from './openapi.js' import BaseJsonService from './base-json.js' const { expect } = chai @@ -376,3 +382,148 @@ describe('category2openapi', function () { ).to.deep.equal(expected) }) }) + +describe('pathParam, pathParams', function () { + it('generates a pathParam with defaults', function () { + const input = { name: 'name', example: 'example' } + const expected = { + name: 'name', + in: 'path', + required: true, + schema: { + type: 'string', + }, + example: 'example', + description: undefined, + } + expect(pathParam(input)).to.deep.equal(expected) + expect(pathParams(input)[0]).to.deep.equal(expected) + }) + + it('generates a pathParam with custom args', function () { + const input = { + name: 'name', + example: true, + schema: { type: 'boolean' }, + description: 'long desc', + } + const expected = { + name: 'name', + in: 'path', + required: true, + schema: { + type: 'boolean', + }, + example: true, + description: 'long desc', + } + expect(pathParam(input)).to.deep.equal(expected) + expect(pathParams(input)[0]).to.deep.equal(expected) + }) + + it('generates multiple pathParams', function () { + expect( + pathParams( + { name: 'name1', example: 'example1' }, + { name: 'name2', example: 'example2' }, + ), + ).to.deep.equal([ + { + name: 'name1', + in: 'path', + required: true, + schema: { + type: 'string', + }, + example: 'example1', + description: undefined, + }, + { + name: 'name2', + in: 'path', + required: true, + schema: { + type: 'string', + }, + example: 'example2', + description: undefined, + }, + ]) + }) +}) + +describe('queryParam, queryParams', function () { + it('generates a queryParam with defaults', function () { + const input = { name: 'name', example: 'example' } + const expected = { + name: 'name', + in: 'query', + required: false, + schema: { type: 'string' }, + example: 'example', + description: undefined, + } + expect(queryParam(input)).to.deep.equal(expected) + expect(queryParams(input)[0]).to.deep.equal(expected) + }) + + it('generates queryParam with custom args', function () { + const input = { + name: 'name', + example: 'example', + required: true, + description: 'long desc', + } + const expected = { + name: 'name', + in: 'query', + required: true, + schema: { type: 'string' }, + example: 'example', + description: 'long desc', + } + expect(queryParam(input)).to.deep.equal(expected) + expect(queryParams(input)[0]).to.deep.equal(expected) + }) + + it('generates a queryParam with boolean/null example', function () { + const input = { name: 'name', example: null, schema: { type: 'boolean' } } + const expected = { + name: 'name', + in: 'query', + required: false, + schema: { type: 'boolean' }, + allowEmptyValue: true, + example: null, + description: undefined, + } + expect(queryParam(input)).to.deep.equal(expected) + expect(queryParams(input)[0]).to.deep.equal(expected) + }) + + it('generates multiple queryParams', function () { + expect( + queryParams( + { name: 'name1', example: 'example1' }, + { name: 'name2', example: 'example2' }, + ), + ).to.deep.equal([ + { + name: 'name1', + in: 'query', + required: false, + schema: { type: 'string' }, + example: 'example1', + description: undefined, + }, + { + name: 'name2', + in: 'query', + required: false, + schema: { type: 'string' }, + example: 'example2', + description: undefined, + }, + ]) + }) +}) diff --git a/core/base-service/service-definitions.js b/core/base-service/service-definitions.js index 6687e5bd61fa4..c4beb656361c6 100644 --- a/core/base-service/service-definitions.js +++ b/core/base-service/service-definitions.js @@ -48,7 +48,7 @@ const serviceDefinition = Joi.object({ Joi.object({ get: Joi.object({ summary: Joi.string().required(), - description: Joi.string().required(), + description: Joi.string(), parameters: Joi.array() .items( Joi.object({ @@ -56,8 +56,12 @@ const serviceDefinition = Joi.object({ description: Joi.string(), in: Joi.string().valid('query', 'path').required(), required: Joi.boolean().required(), - schema: Joi.object({ type: Joi.string().required() }).required(), - example: Joi.string(), + schema: Joi.object({ + type: Joi.string().required(), + enum: Joi.array(), + }).required(), + allowEmptyValue: Joi.boolean(), + example: Joi.string().allow(null), }), ) .min(1) @@ -67,8 +71,8 @@ const serviceDefinition = Joi.object({ ), }).required() -function assertValidServiceDefinition(example, message = undefined) { - Joi.assert(example, serviceDefinition, message) +function assertValidServiceDefinition(service, message = undefined) { + Joi.assert(service, serviceDefinition, message) } const serviceDefinitionExport = Joi.object({ diff --git a/services/amo/amo-downloads.service.js b/services/amo/amo-downloads.service.js index ae61a534e0106..d8ca97447c50d 100644 --- a/services/amo/amo-downloads.service.js +++ b/services/amo/amo-downloads.service.js @@ -1,8 +1,8 @@ import { renderDownloadsBadge } from '../downloads.js' -import { redirector } from '../index.js' -import { BaseAmoService, keywords } from './amo-base.js' +import { redirector, pathParams } from '../index.js' +import { BaseAmoService } from './amo-base.js' -const documentation = ` +const description = ` Previously \`amo/d\` provided a “total downloads” badge. However, [updates to the v3 API](https://github.com/badges/shields/issues/3079) only give us weekly downloads. The route \`amo/d\` redirects to \`amo/dw\`. @@ -12,15 +12,15 @@ class AmoWeeklyDownloads extends BaseAmoService { static category = 'downloads' static route = { base: 'amo/dw', pattern: ':addonId' } - static examples = [ - { - title: 'Mozilla Add-on', - namedParams: { addonId: 'dustman' }, - staticPreview: this.render({ downloads: 120 }), - keywords, - documentation, + static openApi = { + '/amo/dw/{addonId}': { + get: { + summary: 'Mozilla Add-on Downloads', + description, + parameters: pathParams({ name: 'addonId', example: 'dustman' }), + }, }, - ] + } static _cacheLength = 21600 diff --git a/services/amo/amo-rating.service.js b/services/amo/amo-rating.service.js index 57ad89be902ab..5930c1dd0f38b 100644 --- a/services/amo/amo-rating.service.js +++ b/services/amo/amo-rating.service.js @@ -1,27 +1,26 @@ import { starRating } from '../text-formatters.js' import { floorCount as floorCountColor } from '../color-formatters.js' -import { BaseAmoService, keywords } from './amo-base.js' +import { pathParams } from '../index.js' +import { BaseAmoService } from './amo-base.js' export default class AmoRating extends BaseAmoService { static category = 'rating' static route = { base: 'amo', pattern: ':format(stars|rating)/:addonId' } - static examples = [ - { - title: 'Mozilla Add-on', - pattern: 'rating/:addonId', - namedParams: { addonId: 'dustman' }, - staticPreview: this.render({ format: 'rating', rating: 4 }), - keywords, + static openApi = { + '/amo/rating/{addonId}': { + get: { + summary: 'Mozilla Add-on Rating', + parameters: pathParams({ name: 'addonId', example: 'dustman' }), + }, }, - { - title: 'Mozilla Add-on', - pattern: 'stars/:addonId', - namedParams: { addonId: 'dustman' }, - staticPreview: this.render({ format: 'stars', rating: 4 }), - keywords, + '/amo/stars/{addonId}': { + get: { + summary: 'Mozilla Add-on Stars', + parameters: pathParams({ name: 'addonId', example: 'dustman' }), + }, }, - ] + } static _cacheLength = 7200 diff --git a/services/amo/amo-users.service.js b/services/amo/amo-users.service.js index 0a841525bbb21..0a4b4be526dd7 100644 --- a/services/amo/amo-users.service.js +++ b/services/amo/amo-users.service.js @@ -1,18 +1,19 @@ import { renderDownloadsBadge } from '../downloads.js' -import { BaseAmoService, keywords } from './amo-base.js' +import { pathParams } from '../index.js' +import { BaseAmoService } from './amo-base.js' export default class AmoUsers extends BaseAmoService { static category = 'downloads' static route = { base: 'amo/users', pattern: ':addonId' } - static examples = [ - { - title: 'Mozilla Add-on', - namedParams: { addonId: 'dustman' }, - staticPreview: this.render({ users: 750 }), - keywords, + static openApi = { + '/amo/users/{addonId}': { + get: { + summary: 'Mozilla Add-on Users', + parameters: pathParams({ name: 'addonId', example: 'dustman' }), + }, }, - ] + } static _cacheLength = 21600 diff --git a/services/amo/amo-version.service.js b/services/amo/amo-version.service.js index 96394bba7fc44..219692437d0d4 100644 --- a/services/amo/amo-version.service.js +++ b/services/amo/amo-version.service.js @@ -1,18 +1,19 @@ import { renderVersionBadge } from '../version.js' -import { BaseAmoService, keywords } from './amo-base.js' +import { pathParams } from '../index.js' +import { BaseAmoService } from './amo-base.js' export default class AmoVersion extends BaseAmoService { static category = 'version' static route = { base: 'amo/v', pattern: ':addonId' } - static examples = [ - { - title: 'Mozilla Add-on', - namedParams: { addonId: 'dustman' }, - staticPreview: renderVersionBadge({ version: '2.1.0' }), - keywords, + static openApi = { + '/amo/v/{addonId}': { + get: { + summary: 'Mozilla Add-on Version', + parameters: pathParams({ name: 'addonId', example: 'dustman' }), + }, }, - ] + } async handle({ addonId }) { const data = await this.fetch({ addonId }) diff --git a/services/ansible/ansible-collection.service.js b/services/ansible/ansible-collection.service.js index ae7cb07062970..131f86206eac8 100644 --- a/services/ansible/ansible-collection.service.js +++ b/services/ansible/ansible-collection.service.js @@ -1,5 +1,5 @@ import Joi from 'joi' -import { BaseJsonService } from '../index.js' +import { BaseJsonService, pathParams } from '../index.js' const ansibleCollectionSchema = Joi.object({ name: Joi.string().required(), @@ -12,15 +12,14 @@ class AnsibleGalaxyCollectionName extends BaseJsonService { static category = 'other' static route = { base: 'ansible/collection', pattern: ':collectionId' } - static examples = [ - { - title: 'Ansible Collection', - namedParams: { collectionId: '278' }, - staticPreview: this.render({ - name: 'community.general', - }), + static openApi = { + '/ansible/collection/{collectionId}': { + get: { + summary: 'Ansible Collection', + parameters: pathParams({ name: 'collectionId', example: '278' }), + }, }, - ] + } static defaultBadgeData = { label: 'collection' } diff --git a/services/ansible/ansible-quality.service.js b/services/ansible/ansible-quality.service.js index 6af7ae16ca588..249d456e04fa1 100644 --- a/services/ansible/ansible-quality.service.js +++ b/services/ansible/ansible-quality.service.js @@ -1,6 +1,6 @@ import Joi from 'joi' import { floorCount } from '../color-formatters.js' -import { BaseJsonService, InvalidResponse } from '../index.js' +import { BaseJsonService, InvalidResponse, pathParams } from '../index.js' const ansibleContentSchema = Joi.object({ quality_score: Joi.number().allow(null).required(), @@ -20,15 +20,14 @@ export default class AnsibleGalaxyContentQualityScore extends AnsibleGalaxyConte static category = 'analysis' static route = { base: 'ansible/quality', pattern: ':projectId' } - static examples = [ - { - title: 'Ansible Quality Score', - namedParams: { - projectId: '432', + static openApi = { + '/ansible/quality/{projectId}': { + get: { + summary: 'Ansible Quality Score', + parameters: pathParams({ name: 'projectId', example: '432' }), }, - staticPreview: this.render({ qualityScore: 4.125 }), }, - ] + } static defaultBadgeData = { label: 'quality' } diff --git a/services/ansible/ansible-role.service.js b/services/ansible/ansible-role.service.js index ffc27193dc13d..acf5ccce42bee 100644 --- a/services/ansible/ansible-role.service.js +++ b/services/ansible/ansible-role.service.js @@ -1,7 +1,7 @@ import Joi from 'joi' import { renderDownloadsBadge } from '../downloads.js' import { nonNegativeInteger } from '../validators.js' -import { BaseJsonService } from '../index.js' +import { BaseJsonService, pathParams } from '../index.js' const ansibleRoleSchema = Joi.object({ download_count: nonNegativeInteger, @@ -27,13 +27,14 @@ class AnsibleGalaxyRoleDownloads extends AnsibleGalaxyRole { static category = 'downloads' static route = { base: 'ansible/role/d', pattern: ':roleId' } - static examples = [ - { - title: 'Ansible Role', - namedParams: { roleId: '3078' }, - staticPreview: renderDownloadsBadge({ downloads: 76 }), + static openApi = { + '/ansible/role/d/{roleId}': { + get: { + summary: 'Ansible Role', + parameters: pathParams({ name: 'roleId', example: '3078' }), + }, }, - ] + } static defaultBadgeData = { label: 'role downloads' } @@ -47,15 +48,14 @@ class AnsibleGalaxyRoleName extends AnsibleGalaxyRole { static category = 'other' static route = { base: 'ansible/role', pattern: ':roleId' } - static examples = [ - { - title: 'Ansible Role', - namedParams: { roleId: '3078' }, - staticPreview: this.render({ - name: 'ansible-roles.sublimetext3_packagecontrol', - }), + static openApi = { + '/ansible/role/{roleId}': { + get: { + summary: 'Ansible Galaxy Role Name', + parameters: pathParams({ name: 'roleId', example: '3078' }), + }, }, - ] + } static defaultBadgeData = { label: 'role' } diff --git a/services/appveyor/appveyor-build.service.js b/services/appveyor/appveyor-build.service.js index 29cb395835394..cc71150448669 100644 --- a/services/appveyor/appveyor-build.service.js +++ b/services/appveyor/appveyor-build.service.js @@ -1,23 +1,31 @@ import { renderBuildStatusBadge } from '../build-status.js' +import { pathParams } from '../index.js' import AppVeyorBase from './appveyor-base.js' export default class AppVeyorBuild extends AppVeyorBase { static route = this.buildRoute('appveyor/build') - static examples = [ - { - title: 'AppVeyor', - pattern: ':user/:repo', - namedParams: { user: 'gruntjs', repo: 'grunt' }, - staticPreview: this.render({ status: 'success' }), + static openApi = { + '/appveyor/build/{user}/{repo}': { + get: { + summary: 'AppVeyor Build', + parameters: pathParams( + { name: 'user', example: 'gruntjs' }, + { name: 'repo', example: 'grunt' }, + ), + }, }, - { - title: 'AppVeyor branch', - pattern: ':user/:repo/:branch', - namedParams: { user: 'gruntjs', repo: 'grunt', branch: 'master' }, - staticPreview: this.render({ status: 'success' }), + '/appveyor/build/{user}/{repo}/{branch}': { + get: { + summary: 'AppVeyor Build (with branch)', + parameters: pathParams( + { name: 'user', example: 'gruntjs' }, + { name: 'repo', example: 'grunt' }, + { name: 'branch', example: 'master' }, + ), + }, }, - ] + } static render({ status }) { return renderBuildStatusBadge({ status }) diff --git a/services/appveyor/appveyor-job-build.service.js b/services/appveyor/appveyor-job-build.service.js index fc856aa7f25a2..59f5278ec8c38 100644 --- a/services/appveyor/appveyor-job-build.service.js +++ b/services/appveyor/appveyor-job-build.service.js @@ -1,5 +1,5 @@ import { renderBuildStatusBadge } from '../build-status.js' -import { NotFound } from '../index.js' +import { NotFound, pathParams } from '../index.js' import AppVeyorBase from './appveyor-base.js' export default class AppVeyorJobBuild extends AppVeyorBase { @@ -8,29 +8,29 @@ export default class AppVeyorJobBuild extends AppVeyorBase { pattern: ':user/:repo/:job/:branch*', } - static examples = [ - { - title: 'AppVeyor Job', - pattern: ':user/:repo/:job', - namedParams: { - user: 'wpmgprostotema', - repo: 'voicetranscoder', - job: 'Linux', + static openApi = { + '/appveyor/job/build/{user}/{repo}/{job}': { + get: { + summary: 'AppVeyor Job', + parameters: pathParams( + { name: 'user', example: 'wpmgprostotema' }, + { name: 'repo', example: 'voicetranscoder' }, + { name: 'job', example: 'Linux' }, + ), }, - staticPreview: renderBuildStatusBadge({ status: 'success' }), }, - { - title: 'AppVeyor Job branch', - pattern: ':user/:repo/:job/:branch', - namedParams: { - user: 'wpmgprostotema', - repo: 'voicetranscoder', - job: 'Windows', - branch: 'master', + '/appveyor/job/build/{user}/{repo}/{job}/{branch}': { + get: { + summary: 'AppVeyor Job (with branch)', + parameters: pathParams( + { name: 'user', example: 'wpmgprostotema' }, + { name: 'repo', example: 'voicetranscoder' }, + { name: 'job', example: 'Windows' }, + { name: 'branch', example: 'master' }, + ), }, - staticPreview: renderBuildStatusBadge({ status: 'success' }), }, - ] + } transform({ data, jobName }) { if (!('build' in data)) { diff --git a/services/appveyor/appveyor-tests.service.js b/services/appveyor/appveyor-tests.service.js index 05df133dfc843..2bef4b4cc0798 100644 --- a/services/appveyor/appveyor-tests.service.js +++ b/services/appveyor/appveyor-tests.service.js @@ -1,18 +1,12 @@ import { testResultQueryParamSchema, + testResultOpenApiQueryParams, renderTestResultBadge, - documentation, + documentation as description, } from '../test-results.js' +import { pathParams } from '../index.js' import AppVeyorBase from './appveyor-base.js' -const commonPreviewProps = { - passed: 477, - failed: 2, - skipped: 0, - total: 479, - isCompact: false, -} - export default class AppVeyorTests extends AppVeyorBase { static category = 'test-results' static route = { @@ -20,63 +14,35 @@ export default class AppVeyorTests extends AppVeyorBase { queryParamSchema: testResultQueryParamSchema, } - static examples = [ - { - title: 'AppVeyor tests', - pattern: ':user/:repo', - namedParams: { - user: 'NZSmartie', - repo: 'coap-net-iu0to', - }, - staticPreview: this.render(commonPreviewProps), - documentation, - }, - { - title: 'AppVeyor tests (branch)', - pattern: ':user/:repo/:branch', - namedParams: { - user: 'NZSmartie', - repo: 'coap-net-iu0to', - branch: 'master', + static openApi = { + '/appveyor/tests/{user}/{repo}': { + get: { + summary: 'AppVeyor tests', + description, + parameters: [ + ...pathParams( + { name: 'user', example: 'NZSmartie' }, + { name: 'repo', example: 'coap-net-iu0to' }, + ), + ...testResultOpenApiQueryParams, + ], }, - staticPreview: this.render(commonPreviewProps), - documentation, }, - { - title: 'AppVeyor tests (compact)', - pattern: ':user/:repo', - namedParams: { - user: 'NZSmartie', - repo: 'coap-net-iu0to', - }, - queryParams: { compact_message: null }, - staticPreview: this.render({ - ...commonPreviewProps, - isCompact: true, - }), - documentation, - }, - { - title: 'AppVeyor tests with custom labels', - pattern: ':user/:repo', - namedParams: { - user: 'NZSmartie', - repo: 'coap-net-iu0to', - }, - queryParams: { - passed_label: 'good', - failed_label: 'bad', - skipped_label: 'n/a', + '/appveyor/tests/{user}/{repo}/{branch}': { + get: { + summary: 'AppVeyor tests (with branch)', + description, + parameters: [ + ...pathParams( + { name: 'user', example: 'NZSmartie' }, + { name: 'repo', example: 'coap-net-iu0to' }, + { name: 'branch', example: 'master' }, + ), + ...testResultOpenApiQueryParams, + ], }, - staticPreview: this.render({ - ...commonPreviewProps, - passedLabel: 'good', - failedLabel: 'bad', - skippedLabel: 'n/a', - }), - documentation, }, - ] + } static defaultBadgeData = { label: 'tests', diff --git a/services/dynamic/dynamic-json.service.js b/services/dynamic/dynamic-json.service.js index 68ffd4f6dc54d..f730395073471 100644 --- a/services/dynamic/dynamic-json.service.js +++ b/services/dynamic/dynamic-json.service.js @@ -1,5 +1,5 @@ import { MetricNames } from '../../core/base-service/metric-helper.js' -import { BaseJsonService } from '../index.js' +import { BaseJsonService, queryParams } from '../index.js' import { createRoute } from './dynamic-helpers.js' import jsonPath from './json-path.js' @@ -14,13 +14,11 @@ export default class DynamicJson extends jsonPath(BaseJsonService) { The Dynamic JSON Badge allows you to extract an arbitrary value from any JSON Document using a JSONPath selector and show it on a badge.

`, - parameters: [ + parameters: queryParams( { name: 'url', description: 'The URL to a JSON document', - in: 'query', required: true, - schema: { type: 'string' }, example: 'https://github.com/badges/shields/raw/master/package.json', }, @@ -28,28 +26,20 @@ export default class DynamicJson extends jsonPath(BaseJsonService) { name: 'query', description: 'A JSONPath expression that will be used to query the document', - in: 'query', required: true, - schema: { type: 'string' }, example: '$.name', }, { name: 'prefix', description: 'Optional prefix to append to the value', - in: 'query', - required: false, - schema: { type: 'string' }, example: '[', }, { name: 'suffix', description: 'Optional suffix to append to the value', - in: 'query', - required: false, - schema: { type: 'string' }, example: ']', }, - ], + ), }, }, } diff --git a/services/dynamic/dynamic-xml.service.js b/services/dynamic/dynamic-xml.service.js index fd4216c5a0302..cd84d2f547868 100644 --- a/services/dynamic/dynamic-xml.service.js +++ b/services/dynamic/dynamic-xml.service.js @@ -2,7 +2,12 @@ import { DOMParser } from '@xmldom/xmldom' import xpath from 'xpath' import { MetricNames } from '../../core/base-service/metric-helper.js' import { renderDynamicBadge, httpErrors } from '../dynamic-common.js' -import { BaseService, InvalidResponse, InvalidParameter } from '../index.js' +import { + BaseService, + InvalidResponse, + InvalidParameter, + queryParams, +} from '../index.js' import { createRoute } from './dynamic-helpers.js' // This service extends BaseService because it uses a different XML parser @@ -23,41 +28,31 @@ export default class DynamicXml extends BaseService { The Dynamic XML Badge allows you to extract an arbitrary value from any XML Document using an XPath selector and show it on a badge.

`, - parameters: [ + parameters: queryParams( { name: 'url', description: 'The URL to a XML document', - in: 'query', required: true, - schema: { type: 'string' }, example: 'https://httpbin.org/xml', }, { name: 'query', description: 'A XPath expression that will be used to query the document', - in: 'query', required: true, - schema: { type: 'string' }, example: '//slideshow/slide[1]/title', }, { name: 'prefix', description: 'Optional prefix to append to the value', - in: 'query', - required: false, - schema: { type: 'string' }, example: '[', }, { name: 'suffix', description: 'Optional suffix to append to the value', - in: 'query', - required: false, - schema: { type: 'string' }, example: ']', }, - ], + ), }, }, } diff --git a/services/dynamic/dynamic-yaml.service.js b/services/dynamic/dynamic-yaml.service.js index c1a5ab29ce229..0857c8ef144b4 100644 --- a/services/dynamic/dynamic-yaml.service.js +++ b/services/dynamic/dynamic-yaml.service.js @@ -1,5 +1,5 @@ import { MetricNames } from '../../core/base-service/metric-helper.js' -import { BaseYamlService } from '../index.js' +import { BaseYamlService, queryParams } from '../index.js' import { createRoute } from './dynamic-helpers.js' import jsonPath from './json-path.js' @@ -14,13 +14,11 @@ export default class DynamicYaml extends jsonPath(BaseYamlService) { The Dynamic YAML Badge allows you to extract an arbitrary value from any YAML Document using a JSONPath selector and show it on a badge.

`, - parameters: [ + parameters: queryParams( { name: 'url', description: 'The URL to a YAML document', - in: 'query', required: true, - schema: { type: 'string' }, example: 'https://raw.githubusercontent.com/badges/shields/master/.github/dependabot.yml', }, @@ -28,28 +26,20 @@ export default class DynamicYaml extends jsonPath(BaseYamlService) { name: 'query', description: 'A JSONPath expression that will be used to query the document', - in: 'query', required: true, - schema: { type: 'string' }, example: '$.version', }, { name: 'prefix', description: 'Optional prefix to append to the value', - in: 'query', - required: false, - schema: { type: 'string' }, example: '[', }, { name: 'suffix', description: 'Optional suffix to append to the value', - in: 'query', - required: false, - schema: { type: 'string' }, example: ']', }, - ], + ), }, }, } diff --git a/services/endpoint/endpoint.service.js b/services/endpoint/endpoint.service.js index 46bc6c8412615..33d4788b02804 100644 --- a/services/endpoint/endpoint.service.js +++ b/services/endpoint/endpoint.service.js @@ -3,7 +3,7 @@ import Joi from 'joi' import { httpErrors } from '../dynamic-common.js' import { optionalUrl } from '../validators.js' import { fetchEndpointData } from '../endpoint-common.js' -import { BaseJsonService, InvalidParameter } from '../index.js' +import { BaseJsonService, InvalidParameter, queryParams } from '../index.js' const blockedDomains = ['github.com', 'shields.io'] @@ -135,16 +135,12 @@ export default class Endpoint extends BaseJsonService { get: { summary: 'Endpoint Badge', description, - parameters: [ - { - name: 'url', - description: 'The URL to your JSON endpoint', - in: 'query', - required: true, - schema: { type: 'string' }, - example: 'https://shields.redsparr0w.com/2473/monday', - }, - ], + parameters: queryParams({ + name: 'url', + description: 'The URL to your JSON endpoint', + required: true, + example: 'https://shields.redsparr0w.com/2473/monday', + }), }, }, } diff --git a/services/test-results.js b/services/test-results.js index 84f6831762087..93e21c3f36a9a 100644 --- a/services/test-results.js +++ b/services/test-results.js @@ -1,4 +1,5 @@ import Joi from 'joi' +import { queryParams } from './index.js' const testResultQueryParamSchema = Joi.object({ compact_message: Joi.equal(''), @@ -7,6 +8,17 @@ const testResultQueryParamSchema = Joi.object({ skipped_label: Joi.string(), }).required() +const testResultOpenApiQueryParams = queryParams( + { + name: 'compact_message', + example: null, + schema: { type: 'boolean' }, + }, + { name: 'passed_label', example: 'good' }, + { name: 'failed_label', example: 'bad' }, + { name: 'skipped_label', example: 'n/a' }, +) + function renderTestResultMessage({ passed, failed, @@ -89,13 +101,13 @@ const documentation = `

For example, if you want to use a different terminology: -
+
?passed_label=good&failed_label=bad&skipped_label=n%2Fa

Or symbols: -
+
?compact_message&passed_label=💃&failed_label=🤦‍♀️&skipped_label=🤷

@@ -106,6 +118,7 @@ const documentation = ` export { testResultQueryParamSchema, + testResultOpenApiQueryParams, renderTestResultMessage, renderTestResultBadge, documentation,