diff --git a/core/base-service/base.js b/core/base-service/base.js
index 02eefc59b1..4b7e460955 100644
--- a/core/base-service/base.js
+++ b/core/base-service/base.js
@@ -19,7 +19,6 @@ import {
InvalidParameter,
Deprecated,
} from './errors.js'
-import { validateExample, transformExample } from './examples.js'
import { fetch } from './got.js'
import { getEnum } from './openapi.js'
import {
@@ -144,31 +143,14 @@ class BaseService {
static auth = undefined
/**
- * Array of Example objects describing example URLs for this service.
- * These should use the format specified in `route`,
- * and can be used to demonstrate how to use badges for this service.
- *
- * The preferred way to specify an example is with `namedParams` which are
- * substituted into the service's compiled route pattern. The rendered badge
- * is specified with `staticPreview`.
- *
- * For services which use a route `format`, the `pattern` can be specified as
- * part of the example.
- *
- * @see {@link module:core/base-service/base~Example}
- * @abstract
- * @type {module:core/base-service/base~Example[]}
- */
- static examples = []
-
- /**
- * Optional: an OpenAPI Paths Object describing this service's
+ * An OpenAPI Paths Object describing this service's
* route or routes in OpenAPI format.
*
- * @see https://swagger.io/specification/#paths-object
* @abstract
+ * @see https://swagger.io/specification/#paths-object
+ * @type {module:core/base-service/service-definitions~openApiSchema}
*/
- static openApi = undefined
+ static openApi = {}
static get _cacheLength() {
const cacheLengths = {
@@ -207,23 +189,17 @@ class BaseService {
`Default badge data for ${this.name}`,
)
- 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}`,
- )
- }
+ 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}`,
+ )
}
}
}
@@ -233,10 +209,6 @@ class BaseService {
const { base, format, pattern } = this.route
const queryParams = getQueryParamNames(this.route)
- const examples = this.examples.map((example, index) =>
- transformExample(example, index, this),
- )
-
let route
if (pattern) {
route = { pattern: makeFullUrl(base, pattern), queryParams }
@@ -246,7 +218,7 @@ class BaseService {
route = undefined
}
- const result = { category, name, isDeprecated, route, examples, openApi }
+ const result = { category, name, isDeprecated, route, openApi }
assertValidServiceDefinition(result, `getDefinition() for ${this.name}`)
@@ -597,9 +569,11 @@ class BaseService {
* receives numeric can use `Joi.string()`. A boolean
* parameter should use `Joi.equal('')` and will receive an
* empty string on e.g. `?compact_message` and undefined
- * when the parameter is absent. (Note that in,
- * `examples.queryParams` boolean query params should be given
- * `null` values.)
+ * when the parameter is absent. In the OpenApi definitions,
+ * this type of param should be documented as
+ * queryParam({
+ * name: 'compact_message', schema: { type: 'boolean' }, example: null
+ * })
*/
/**
@@ -614,30 +588,4 @@ class BaseService {
* configured credentials are present.
*/
-/**
- * @typedef {object} Example
- * @property {string} title
- * Descriptive text that will be shown next to the badge. The default
- * is to use the service class name, which probably is not what you want.
- * @property {object} namedParams
- * An object containing the values of named parameters to
- * substitute into the compiled route pattern.
- * @property {object} queryParams
- * An object containing query parameters to include in the
- * example URLs. For alphanumeric query parameters, specify a string value.
- * For boolean query parameters, specify `null`.
- * @property {string} pattern
- * The route pattern to compile. Defaults to `this.route.pattern`.
- * @property {object} staticPreview
- * A rendered badge of the sort returned by `handle()` or
- * `render()`: an object containing `message` and optional `label` and
- * `color`. This is usually generated by invoking `this.render()` with some
- * explicit props.
- * @property {string[]} keywords
- * Additional keywords, other than words in the title. This helps
- * users locate relevant badges.
- * @property {string} documentation
- * An HTML string that is included in the badge popup.
- */
-
export default BaseService
diff --git a/core/base-service/base.spec.js b/core/base-service/base.spec.js
index 0fda208a70..575f0386cc 100644
--- a/core/base-service/base.spec.js
+++ b/core/base-service/base.spec.js
@@ -4,6 +4,7 @@ import sinon from 'sinon'
import prometheus from 'prom-client'
import chaiAsPromised from 'chai-as-promised'
import PrometheusMetrics from '../server/prometheus-metrics.js'
+import { pathParam, queryParam } from './openapi.js'
import trace from './trace.js'
import {
NotFound,
@@ -31,14 +32,17 @@ class DummyService extends BaseService {
static category = 'other'
static route = { base: 'foo', pattern: ':namedParamA', queryParamSchema }
- static examples = [
- {
- pattern: ':world',
- namedParams: { world: 'World' },
- staticPreview: this.render({ namedParamA: 'foo', queryParamA: 'bar' }),
- keywords: ['hello'],
+ static openApi = {
+ '/foo/{namedParamA}': {
+ get: {
+ summary: 'Dummy Service',
+ parameters: [
+ pathParam({ name: 'namedParamA', example: 'foo' }),
+ queryParam({ name: 'queryParamA', example: 'bar' }),
+ ],
+ },
},
- ]
+ }
static defaultBadgeData = { label: 'cat', namedLogo: 'appveyor' }
@@ -383,7 +387,7 @@ describe('BaseService', function () {
describe('getDefinition', function () {
it('returns the expected result', function () {
- const { category, name, isDeprecated, route, examples } =
+ const { category, name, isDeprecated, route, openApi } =
DummyService.getDefinition()
expect({
category,
@@ -400,7 +404,7 @@ describe('BaseService', function () {
},
})
// The in-depth tests for examples reside in examples.spec.js
- expect(examples).to.have.lengthOf(1)
+ expect(Object.keys(openApi)).to.have.lengthOf(1)
})
})
diff --git a/core/base-service/deprecated-service.js b/core/base-service/deprecated-service.js
index 341c4843ec..8a43bb014f 100644
--- a/core/base-service/deprecated-service.js
+++ b/core/base-service/deprecated-service.js
@@ -10,14 +10,12 @@ const attrSchema = Joi.object({
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(
+ const { route, name, label, category, message } = Joi.attempt(
attrs,
attrSchema,
`Deprecated service for ${attrs.route.base}`,
@@ -33,7 +31,6 @@ function deprecatedService(attrs) {
static category = category
static isDeprecated = true
static route = route
- static examples = examples
static defaultBadgeData = { label }
async handle() {
diff --git a/core/base-service/deprecated-service.spec.js b/core/base-service/deprecated-service.spec.js
index 58ca62b6d9..870e9622f9 100644
--- a/core/base-service/deprecated-service.spec.js
+++ b/core/base-service/deprecated-service.spec.js
@@ -36,16 +36,6 @@ describe('DeprecatedService', function () {
expect(service.category).to.equal(category)
})
- it('sets specified examples', function () {
- const examples = [
- {
- title: 'Not sure we would have 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({ ...commonAttrs })
expect(await service.invoke()).to.deep.equal({
diff --git a/core/base-service/examples.js b/core/base-service/examples.js
deleted file mode 100644
index cf1b1c021d..0000000000
--- a/core/base-service/examples.js
+++ /dev/null
@@ -1,155 +0,0 @@
-import Joi from 'joi'
-import { pathToRegexp, compile } from 'path-to-regexp'
-import categories from '../../services/categories.js'
-import coalesceBadge from './coalesce-badge.js'
-import { makeFullUrl } from './route.js'
-
-const optionalObjectOfKeyValues = Joi.object().pattern(
- /./,
- Joi.string().allow(null),
-)
-
-const schema = Joi.object({
- // This should be:
- // title: Joi.string().required(),
- title: Joi.string(),
- namedParams: optionalObjectOfKeyValues.required(),
- queryParams: optionalObjectOfKeyValues.default({}),
- pattern: Joi.string(),
- staticPreview: Joi.object({
- label: Joi.string(),
- message: Joi.alternatives()
- .try(Joi.string().allow('').required(), Joi.number())
- .required(),
- color: Joi.string(),
- style: Joi.string(),
- }).required(),
- keywords: Joi.array().items(Joi.string()).default([]),
- documentation: Joi.string(), // Valid HTML.
-}).required()
-
-function validateExample(example, index, ServiceClass) {
- const result = Joi.attempt(
- example,
- schema,
- `Example for ${ServiceClass.name} at index ${index}`,
- )
-
- const { pattern, namedParams } = result
-
- if (!pattern && !ServiceClass.route.pattern) {
- throw new Error(
- `Example for ${ServiceClass.name} at index ${index} does not declare a pattern`,
- )
- }
- if (pattern === ServiceClass.route.pattern) {
- throw new Error(
- `Example for ${ServiceClass.name} at index ${index} declares a redundant pattern which should be removed`,
- )
- }
-
- // Make sure we can build the full URL using these patterns.
- try {
- compile(pattern || ServiceClass.route.pattern, {
- encode: encodeURIComponent,
- })(namedParams)
- } catch (e) {
- throw Error(
- `In example for ${
- ServiceClass.name
- } at index ${index}, ${e.message.toLowerCase()}`,
- )
- }
- // Make sure there are no extra keys.
- let keys = []
- pathToRegexp(pattern || ServiceClass.route.pattern, keys, {
- strict: true,
- sensitive: true,
- })
- keys = keys.map(({ name }) => name)
- const extraKeys = Object.keys(namedParams).filter(k => !keys.includes(k))
- if (extraKeys.length) {
- throw Error(
- `In example for ${
- ServiceClass.name
- } at index ${index}, namedParams contains unknown keys: ${extraKeys.join(
- ', ',
- )}`,
- )
- }
-
- if (example.keywords) {
- // Make sure the keywords are at least two characters long.
- const tinyKeywords = example.keywords.filter(k => k.length < 2)
- if (tinyKeywords.length) {
- throw Error(
- `In example for ${
- ServiceClass.name
- } at index ${index}, keywords contains words that are less than two characters long: ${tinyKeywords.join(
- ', ',
- )}`,
- )
- }
- // Make sure none of the keywords are already included in the title.
- const title = (example.title || ServiceClass.name).toLowerCase()
- const redundantKeywords = example.keywords.filter(k =>
- title.includes(k.toLowerCase()),
- )
- if (redundantKeywords.length) {
- throw Error(
- `In example for ${
- ServiceClass.name
- } at index ${index}, keywords contains words that are already in the title: ${redundantKeywords.join(
- ', ',
- )}`,
- )
- }
- }
-
- return result
-}
-
-function transformExample(inExample, index, ServiceClass) {
- const {
- // We should get rid of this transform, since the class name is never what
- // we want to see.
- title = ServiceClass.name,
- namedParams,
- queryParams,
- pattern,
- staticPreview,
- keywords,
- documentation,
- } = validateExample(inExample, index, ServiceClass)
-
- const { label, message, color, style, namedLogo } = coalesceBadge(
- {},
- staticPreview,
- ServiceClass.defaultBadgeData,
- ServiceClass,
- )
-
- const category = categories.find(c => c.id === ServiceClass.category)
- return {
- title,
- example: {
- pattern: makeFullUrl(
- ServiceClass.route.base,
- pattern || ServiceClass.route.pattern,
- ),
- namedParams,
- queryParams,
- },
- preview: {
- label,
- message: `${message}`,
- color,
- style: style === 'flat' ? undefined : style,
- namedLogo,
- },
- keywords: category ? keywords.concat(category.keywords) : keywords,
- documentation: documentation ? { __html: documentation } : undefined,
- }
-}
-
-export { validateExample, transformExample }
diff --git a/core/base-service/examples.spec.js b/core/base-service/examples.spec.js
deleted file mode 100644
index 7b64b204d4..0000000000
--- a/core/base-service/examples.spec.js
+++ /dev/null
@@ -1,167 +0,0 @@
-import { expect } from 'chai'
-import { test, given } from 'sazerac'
-import { validateExample, transformExample } from './examples.js'
-
-describe('validateExample function', function () {
- it('passes valid examples', function () {
- const validExamples = [
- {
- title: 'Package manager versioning badge',
- staticPreview: { message: '123' },
- pattern: 'dt/:package',
- namedParams: { package: 'mypackage' },
- keywords: ['semver', 'management'],
- },
- ]
-
- validExamples.forEach(example => {
- expect(() =>
- validateExample(example, 0, { route: {}, name: 'mockService' }),
- ).not.to.throw(Error)
- })
- })
-
- it('rejects invalid examples', function () {
- const invalidExamples = [
- {},
- { staticPreview: { message: '123' } },
- {
- staticPreview: { message: '123' },
- pattern: 'dt/:package',
- namedParams: { package: 'mypackage' },
- exampleUrl: 'dt/mypackage',
- },
- { staticPreview: { message: '123' }, pattern: 'dt/:package' },
- {
- staticPreview: { message: '123' },
- pattern: 'dt/:package',
- previewUrl: 'dt/mypackage',
- },
- {
- staticPreview: { message: '123' },
- pattern: 'dt/:package',
- exampleUrl: 'dt/mypackage',
- },
- { previewUrl: 'dt/mypackage' },
- {
- staticPreview: { message: '123' },
- pattern: 'dt/:package',
- namedParams: { package: 'mypackage' },
- keywords: ['a'], // Keyword too short.
- },
- {
- staticPreview: { message: '123' },
- pattern: 'dt/:package',
- namedParams: { package: 'mypackage' },
- keywords: ['mockService'], // No title and keyword matching the class name.
- },
- {
- title: 'Package manager versioning badge',
- staticPreview: { message: '123' },
- pattern: 'dt/:package',
- namedParams: { package: 'mypackage' },
- keywords: ['version'], // Keyword included in title.
- },
- ]
-
- invalidExamples.forEach(example => {
- expect(() =>
- validateExample(example, 0, { route: {}, name: 'mockService' }),
- ).to.throw(Error)
- })
- })
-})
-
-test(transformExample, function () {
- const ExampleService = {
- name: 'ExampleService',
- route: {
- base: 'some-service',
- pattern: ':interval/:packageName',
- },
- defaultBadgeData: {
- label: 'downloads',
- },
- category: 'platform-support',
- }
-
- given(
- {
- pattern: 'dt/:packageName',
- namedParams: { packageName: 'express' },
- staticPreview: { message: '50k' },
- keywords: ['hello'],
- },
- 0,
- ExampleService,
- ).expect({
- title: 'ExampleService',
- example: {
- pattern: '/some-service/dt/:packageName',
- namedParams: { packageName: 'express' },
- queryParams: {},
- },
- preview: {
- label: 'downloads',
- message: '50k',
- color: 'lightgrey',
- namedLogo: undefined,
- style: undefined,
- },
- keywords: ['hello', 'platform'],
- documentation: undefined,
- })
-
- given(
- {
- namedParams: { interval: 'dt', packageName: 'express' },
- staticPreview: { message: '50k' },
- keywords: ['hello'],
- },
- 0,
- ExampleService,
- ).expect({
- title: 'ExampleService',
- example: {
- pattern: '/some-service/:interval/:packageName',
- namedParams: { interval: 'dt', packageName: 'express' },
- queryParams: {},
- },
- preview: {
- label: 'downloads',
- message: '50k',
- color: 'lightgrey',
- namedLogo: undefined,
- style: undefined,
- },
- keywords: ['hello', 'platform'],
- documentation: undefined,
- })
-
- given(
- {
- namedParams: { interval: 'dt', packageName: 'express' },
- queryParams: { registry_url: 'http://example.com/' },
- staticPreview: { message: '50k' },
- keywords: ['hello'],
- },
- 0,
- ExampleService,
- ).expect({
- title: 'ExampleService',
- example: {
- pattern: '/some-service/:interval/:packageName',
- namedParams: { interval: 'dt', packageName: 'express' },
- queryParams: { registry_url: 'http://example.com/' },
- },
- preview: {
- label: 'downloads',
- message: '50k',
- color: 'lightgrey',
- namedLogo: undefined,
- style: undefined,
- },
- keywords: ['hello', 'platform'],
- documentation: undefined,
- })
-})
diff --git a/core/base-service/openapi.js b/core/base-service/openapi.js
index eb6671107e..3eabf9cb99 100644
--- a/core/base-service/openapi.js
+++ b/core/base-service/openapi.js
@@ -46,13 +46,6 @@ function getCodeSamples(altText) {
]
}
-function pattern2openapi(pattern) {
- return pattern
- .replace(/:([A-Za-z0-9_\-.]+)(?=[/]?)/g, (matches, grp1) => `{${grp1}}`)
- .replace(/\([^)]*\)/g, '')
- .replace(/\+$/, '')
-}
-
function getEnum(pattern, paramName) {
const re = new RegExp(`${paramName}\\(([A-Za-z0-9_\\-|]+)\\)`)
const match = pattern.match(re)
@@ -65,126 +58,6 @@ function getEnum(pattern, paramName) {
return match[1].split('|')
}
-function param2openapi(pattern, paramName, exampleValue, paramType) {
- const outParam = {}
- outParam.name = paramName
- // We don't have description if we are building the OpenAPI spec from examples[]
- outParam.in = paramType
- if (paramType === 'path') {
- outParam.required = true
- } else {
- /* Occasionally we do have required query params, but we can't
- detect this if we are building the OpenAPI spec from examples[]
- so just assume all query params are optional */
- outParam.required = false
- }
-
- if (exampleValue === null && paramType === 'query') {
- outParam.schema = { type: 'boolean' }
- outParam.allowEmptyValue = true
- } else {
- outParam.schema = { type: 'string' }
- }
-
- if (paramType === 'path') {
- outParam.schema.enum = getEnum(pattern, paramName)
- }
-
- outParam.example = exampleValue
- return outParam
-}
-
-function getVariants(pattern) {
- /*
- given a URL pattern (which may include '/one/or/:more?/:optional/:parameters*')
- return an array of all possible permutations:
- [
- '/one/or/:more/:optional/:parameters',
- '/one/or/:optional/:parameters',
- '/one/or/:more/:optional',
- '/one/or/:optional',
- ]
- */
- const patterns = [pattern.split('/')]
- while (patterns.flat().find(p => p.endsWith('?') || p.endsWith('*'))) {
- for (let i = 0; i < patterns.length; i++) {
- const pattern = patterns[i]
- for (let j = 0; j < pattern.length; j++) {
- const path = pattern[j]
- if (path.endsWith('?') || path.endsWith('*')) {
- pattern[j] = path.slice(0, -1)
- patterns.push(patterns[i].filter(p => p !== pattern[j]))
- }
- }
- }
- }
- for (let i = 0; i < patterns.length; i++) {
- patterns[i] = patterns[i].join('/')
- }
- return patterns
-}
-
-function examples2openapi(examples) {
- const paths = {}
- for (const example of examples) {
- const patterns = getVariants(example.example.pattern)
-
- for (const pattern of patterns) {
- const openApiPattern = pattern2openapi(pattern)
- if (
- openApiPattern.includes('*') ||
- openApiPattern.includes('?') ||
- openApiPattern.includes('+') ||
- openApiPattern.includes('(')
- ) {
- throw new Error(`unexpected characters in pattern '${openApiPattern}'`)
- }
-
- /*
- There's several things going on in this block:
- 1. Filter out any examples for params that don't appear
- in this variant of the route
- 2. Make sure we add params to the array
- in the same order they appear in the route
- 3. If there are any params we don't have an example value for,
- make sure they still appear in the pathParams array with
- exampleValue == undefined anyway
- */
- const pathParams = []
- for (const param of openApiPattern
- .split('/')
- .filter(p => p.startsWith('{') && p.endsWith('}'))) {
- const paramName = param.slice(1, -1)
- const exampleValue = example.example.namedParams[paramName]
- pathParams.push(param2openapi(pattern, paramName, exampleValue, 'path'))
- }
-
- const queryParams = example.example.queryParams || {}
-
- const parameters = [
- ...pathParams,
- ...Object.entries(queryParams).map(([paramName, exampleValue]) =>
- param2openapi(pattern, paramName, exampleValue, 'query'),
- ),
- ...globalParamRefs,
- ]
- paths[openApiPattern] = {
- get: {
- summary: example.title,
- description: example?.documentation?.__html
- .replace(/
/g, '
') // react does not like
- .replace(/{/g, '{')
- .replace(/}/g, '}')
- .replace(/