mirror of
https://github.com/badges/shields.git
synced 2025-04-18 19:44:04 +03:00
Complete the examples --> openApi migration; affects [node sonar travis wordpress visualstudio librariesio] (#9977)
* you missed one * remove examples from deprecatedService() I'm not going to replace this with openApi We have zero examples of deprecated services that declare examples * remove examples from redirector() * update test * remove compatibility code for converting examples to openApi * remove all the code for handling examples * remove a few bits of redundant code * improve docs for openApi property * last one, I promise
This commit is contained in:
parent
8ab9dfa9a1
commit
9cfd301b82
@ -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
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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({
|
||||
|
@ -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 }
|
@ -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,
|
||||
})
|
||||
})
|
@ -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(/<br>/g, '<br />') // react does not like <br>
|
||||
.replace(/{/g, '{')
|
||||
.replace(/}/g, '}')
|
||||
.replace(/<style>(.|\n)*?<\/style>/, ''), // workaround for w3c-validation TODO: remove later
|
||||
parameters,
|
||||
'x-code-samples': getCodeSamples(example.title),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
function addGlobalProperties(endpoints) {
|
||||
const paths = {}
|
||||
for (const key of Object.keys(endpoints)) {
|
||||
@ -207,24 +80,13 @@ function sortPaths(obj) {
|
||||
function services2openapi(services, sort) {
|
||||
const paths = {}
|
||||
for (const service of services) {
|
||||
if (service.openApi) {
|
||||
// if the service declares its own OpenAPI definition, use that...
|
||||
for (const [key, value] of Object.entries(
|
||||
addGlobalProperties(service.openApi),
|
||||
)) {
|
||||
if (key in paths && key !== '/github/{variant}/{user}/{repo}') {
|
||||
throw new Error(`Conflicting route: ${key}`)
|
||||
}
|
||||
paths[key] = value
|
||||
}
|
||||
} else {
|
||||
// ...otherwise do our best to build one from examples[]
|
||||
for (const [key, value] of Object.entries(
|
||||
examples2openapi(service.examples),
|
||||
)) {
|
||||
// allow conflicting routes for legacy examples
|
||||
paths[key] = value
|
||||
for (const [key, value] of Object.entries(
|
||||
addGlobalProperties(service.openApi),
|
||||
)) {
|
||||
if (key in paths && key !== '/github/{variant}/{user}/{repo}') {
|
||||
throw new Error(`Conflicting route: ${key}`)
|
||||
}
|
||||
paths[key] = value
|
||||
}
|
||||
}
|
||||
return sort ? sortPaths(paths) : paths
|
||||
|
@ -58,27 +58,6 @@ class OpenApiService extends BaseJsonService {
|
||||
}
|
||||
}
|
||||
|
||||
class LegacyService extends BaseJsonService {
|
||||
static category = 'build'
|
||||
static route = { base: 'legacy/service', pattern: ':packageName/:distTag*' }
|
||||
|
||||
// this service defines an Examples Array
|
||||
static examples = [
|
||||
{
|
||||
title: 'LegacyService Title',
|
||||
namedParams: { packageName: 'badge-maker' },
|
||||
staticPreview: { label: 'build', message: 'passing' },
|
||||
documentation: 'LegacyService Description',
|
||||
},
|
||||
{
|
||||
title: 'LegacyService Title (with Tag)',
|
||||
namedParams: { packageName: 'badge-maker', distTag: 'latest' },
|
||||
staticPreview: { label: 'build', message: 'passing' },
|
||||
documentation: 'LegacyService Description (with Tag)',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const expected = {
|
||||
openapi: '3.0.0',
|
||||
info: { version: '1.0.0', title: 'build', license: { name: 'CC0' } },
|
||||
@ -266,105 +245,6 @@ const expected = {
|
||||
],
|
||||
},
|
||||
},
|
||||
'/legacy/service/{packageName}/{distTag}': {
|
||||
get: {
|
||||
summary: 'LegacyService Title (with Tag)',
|
||||
description: 'LegacyService Description (with Tag)',
|
||||
parameters: [
|
||||
{
|
||||
name: 'packageName',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'badge-maker',
|
||||
},
|
||||
{
|
||||
name: 'distTag',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'latest',
|
||||
},
|
||||
{ $ref: '#/components/parameters/style' },
|
||||
{ $ref: '#/components/parameters/logo' },
|
||||
{ $ref: '#/components/parameters/logoColor' },
|
||||
{ $ref: '#/components/parameters/label' },
|
||||
{ $ref: '#/components/parameters/labelColor' },
|
||||
{ $ref: '#/components/parameters/color' },
|
||||
{ $ref: '#/components/parameters/cacheSeconds' },
|
||||
{ $ref: '#/components/parameters/link' },
|
||||
],
|
||||
'x-code-samples': [
|
||||
{ lang: 'URL', label: 'URL', source: '$url' },
|
||||
{
|
||||
lang: 'Markdown',
|
||||
label: 'Markdown',
|
||||
source: '',
|
||||
},
|
||||
{
|
||||
lang: 'reStructuredText',
|
||||
label: 'rSt',
|
||||
source: '.. image:: $url\n :alt: LegacyService Title (with Tag)',
|
||||
},
|
||||
{
|
||||
lang: 'AsciiDoc',
|
||||
label: 'AsciiDoc',
|
||||
source: 'image:$url[LegacyService Title (with Tag)]',
|
||||
},
|
||||
{
|
||||
lang: 'HTML',
|
||||
label: 'HTML',
|
||||
source: '<img alt="LegacyService Title (with Tag)" src="$url">',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'/legacy/service/{packageName}': {
|
||||
get: {
|
||||
summary: 'LegacyService Title (with Tag)',
|
||||
description: 'LegacyService Description (with Tag)',
|
||||
parameters: [
|
||||
{
|
||||
name: 'packageName',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'badge-maker',
|
||||
},
|
||||
{ $ref: '#/components/parameters/style' },
|
||||
{ $ref: '#/components/parameters/logo' },
|
||||
{ $ref: '#/components/parameters/logoColor' },
|
||||
{ $ref: '#/components/parameters/label' },
|
||||
{ $ref: '#/components/parameters/labelColor' },
|
||||
{ $ref: '#/components/parameters/color' },
|
||||
{ $ref: '#/components/parameters/cacheSeconds' },
|
||||
{ $ref: '#/components/parameters/link' },
|
||||
],
|
||||
'x-code-samples': [
|
||||
{ lang: 'URL', label: 'URL', source: '$url' },
|
||||
{
|
||||
lang: 'Markdown',
|
||||
label: 'Markdown',
|
||||
source: '',
|
||||
},
|
||||
{
|
||||
lang: 'reStructuredText',
|
||||
label: 'rSt',
|
||||
source: '.. image:: $url\n :alt: LegacyService Title (with Tag)',
|
||||
},
|
||||
{
|
||||
lang: 'AsciiDoc',
|
||||
label: 'AsciiDoc',
|
||||
source: 'image:$url[LegacyService Title (with Tag)]',
|
||||
},
|
||||
{
|
||||
lang: 'HTML',
|
||||
label: 'HTML',
|
||||
source: '<img alt="LegacyService Title (with Tag)" src="$url">',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -379,10 +259,7 @@ describe('category2openapi', function () {
|
||||
clean(
|
||||
category2openapi({
|
||||
category: { name: 'build' },
|
||||
services: [
|
||||
OpenApiService.getDefinition(),
|
||||
LegacyService.getDefinition(),
|
||||
],
|
||||
services: [OpenApiService.getDefinition()],
|
||||
}),
|
||||
),
|
||||
).to.deep.equal(expected)
|
||||
|
@ -18,7 +18,6 @@ const attrSchema = Joi.object({
|
||||
category: isValidCategory,
|
||||
isDeprecated: Joi.boolean().default(true),
|
||||
route: isValidRoute,
|
||||
examples: Joi.array().has(Joi.object()).default([]),
|
||||
openApi: openApiSchema,
|
||||
transformPath: Joi.func()
|
||||
.maxArity(1)
|
||||
@ -38,7 +37,6 @@ export default function redirector(attrs) {
|
||||
category,
|
||||
isDeprecated,
|
||||
route,
|
||||
examples,
|
||||
openApi,
|
||||
transformPath,
|
||||
transformQueryParams,
|
||||
@ -55,7 +53,6 @@ export default function redirector(attrs) {
|
||||
static category = category
|
||||
static isDeprecated = isDeprecated
|
||||
static route = route
|
||||
static examples = examples
|
||||
static openApi = openApi
|
||||
|
||||
static register({ camp, metricInstance }, { rasterUrl }) {
|
||||
|
@ -45,24 +45,6 @@ describe('Redirector', function () {
|
||||
).to.throw('"dateAdded" is required')
|
||||
})
|
||||
|
||||
it('sets specified example', function () {
|
||||
const examples = [
|
||||
{
|
||||
title: 'very old service',
|
||||
pattern: ':namedParamA',
|
||||
namedParams: {
|
||||
namedParamA: 'namedParamAValue',
|
||||
},
|
||||
staticPreview: {
|
||||
label: 'service',
|
||||
message: 'v0.14.0',
|
||||
color: 'blue',
|
||||
},
|
||||
},
|
||||
]
|
||||
expect(redirector({ ...attrs, examples }).examples).to.equal(examples)
|
||||
})
|
||||
|
||||
describe('ScoutCamp integration', function () {
|
||||
let port, baseUrl
|
||||
beforeEach(async function () {
|
||||
|
@ -1,37 +1,43 @@
|
||||
/**
|
||||
* @module
|
||||
*/
|
||||
import Joi from 'joi'
|
||||
|
||||
const arrayOfStrings = Joi.array().items(Joi.string()).min(0).required()
|
||||
|
||||
const objectOfKeyValues = Joi.object()
|
||||
.pattern(/./, Joi.string().allow(null))
|
||||
.required()
|
||||
|
||||
const openApiSchema = Joi.object().pattern(
|
||||
/./,
|
||||
Joi.object({
|
||||
get: Joi.object({
|
||||
summary: Joi.string().required(),
|
||||
description: Joi.string(),
|
||||
parameters: Joi.array()
|
||||
.items(
|
||||
Joi.object({
|
||||
name: Joi.string().required(),
|
||||
description: Joi.string(),
|
||||
in: Joi.string().valid('query', 'path').required(),
|
||||
required: Joi.boolean().required(),
|
||||
schema: Joi.object({
|
||||
type: Joi.string().required(),
|
||||
enum: Joi.array(),
|
||||
}).required(),
|
||||
allowEmptyValue: Joi.boolean(),
|
||||
example: Joi.string().allow(null),
|
||||
}),
|
||||
)
|
||||
.min(1)
|
||||
.required(),
|
||||
/**
|
||||
* Joi schema describing the subset of OpenAPI paths we use in this application
|
||||
*
|
||||
* @see https://swagger.io/specification/#paths-object
|
||||
*/
|
||||
const openApiSchema = Joi.object()
|
||||
.pattern(
|
||||
/./,
|
||||
Joi.object({
|
||||
get: Joi.object({
|
||||
summary: Joi.string().required(),
|
||||
description: Joi.string(),
|
||||
parameters: Joi.array()
|
||||
.items(
|
||||
Joi.object({
|
||||
name: Joi.string().required(),
|
||||
description: Joi.string(),
|
||||
in: Joi.string().valid('query', 'path').required(),
|
||||
required: Joi.boolean().required(),
|
||||
schema: Joi.object({
|
||||
type: Joi.string().required(),
|
||||
enum: Joi.array(),
|
||||
}).required(),
|
||||
allowEmptyValue: Joi.boolean(),
|
||||
example: Joi.string().allow(null),
|
||||
}),
|
||||
)
|
||||
.min(1)
|
||||
.required(),
|
||||
}).required(),
|
||||
}).required(),
|
||||
}).required(),
|
||||
)
|
||||
)
|
||||
.default({})
|
||||
|
||||
const serviceDefinition = Joi.object({
|
||||
category: Joi.string().required(),
|
||||
@ -47,29 +53,6 @@ const serviceDefinition = Joi.object({
|
||||
queryParams: arrayOfStrings,
|
||||
}),
|
||||
),
|
||||
examples: Joi.array()
|
||||
.items(
|
||||
Joi.object({
|
||||
title: Joi.string().required(),
|
||||
example: Joi.object({
|
||||
pattern: Joi.string(),
|
||||
namedParams: objectOfKeyValues,
|
||||
queryParams: objectOfKeyValues,
|
||||
}).required(),
|
||||
preview: Joi.object({
|
||||
label: Joi.string(),
|
||||
message: Joi.string().allow('').required(),
|
||||
color: Joi.string().required(),
|
||||
style: Joi.string(),
|
||||
namedLogo: Joi.string(),
|
||||
}).required(),
|
||||
keywords: arrayOfStrings,
|
||||
documentation: Joi.object({
|
||||
__html: Joi.string().required(), // Valid HTML.
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.default([]),
|
||||
openApi: openApiSchema,
|
||||
}).required()
|
||||
|
||||
@ -84,15 +67,14 @@ const serviceDefinitionExport = Joi.object({
|
||||
Joi.object({
|
||||
id: Joi.string().required(),
|
||||
name: Joi.string().required(),
|
||||
keywords: arrayOfStrings,
|
||||
}),
|
||||
)
|
||||
.required(),
|
||||
services: Joi.array().items(serviceDefinition).required(),
|
||||
}).required()
|
||||
|
||||
function assertValidServiceDefinitionExport(examples, message = undefined) {
|
||||
Joi.assert(examples, serviceDefinitionExport, message)
|
||||
function assertValidServiceDefinitionExport(openApiSpec, message = undefined) {
|
||||
Joi.assert(openApiSpec, serviceDefinitionExport, message)
|
||||
}
|
||||
|
||||
export {
|
||||
|
@ -350,8 +350,6 @@ Save, run `npm start`, and you can see it [locally](http://127.0.0.1:3000/).
|
||||
|
||||
If you update `openApi`, you don't have to restart the server. Run `npm run prestart` in another terminal window and the frontend will update.
|
||||
|
||||
Note: Some services define this information in an array property called `examples`. This is deprecated and we're in the process of converting them. New services should declare an `openApi` object.
|
||||
|
||||
### (4.5) Write Tests<!-- Change the link below when you change the heading -->
|
||||
|
||||
[write tests]: #45-write-tests
|
||||
|
@ -1,24 +1,23 @@
|
||||
export default [
|
||||
{ id: 'build', name: 'Build', keywords: ['build'] },
|
||||
{ id: 'coverage', name: 'Code Coverage', keywords: ['coverage'] },
|
||||
{ id: 'test-results', name: 'Test Results', keywords: ['tests'] },
|
||||
{ id: 'analysis', name: 'Analysis', keywords: ['analysis'] },
|
||||
{ id: 'chat', name: 'Chat', keywords: ['chat'] },
|
||||
{ id: 'dependencies', name: 'Dependencies', keywords: ['dependencies'] },
|
||||
{ id: 'size', name: 'Size', keywords: ['size'] },
|
||||
{ id: 'downloads', name: 'Downloads', keywords: ['downloads'] },
|
||||
{ id: 'funding', name: 'Funding', keywords: ['funding'] },
|
||||
{ id: 'issue-tracking', name: 'Issue Tracking', keywords: ['issue'] },
|
||||
{ id: 'license', name: 'License', keywords: ['license'] },
|
||||
{ id: 'rating', name: 'Rating', keywords: ['rating'] },
|
||||
{ id: 'social', name: 'Social', keywords: ['social'] },
|
||||
{ id: 'version', name: 'Version', keywords: ['version'] },
|
||||
{ id: 'build', name: 'Build' },
|
||||
{ id: 'coverage', name: 'Code Coverage' },
|
||||
{ id: 'test-results', name: 'Test Results' },
|
||||
{ id: 'analysis', name: 'Analysis' },
|
||||
{ id: 'chat', name: 'Chat' },
|
||||
{ id: 'dependencies', name: 'Dependencies' },
|
||||
{ id: 'size', name: 'Size' },
|
||||
{ id: 'downloads', name: 'Downloads' },
|
||||
{ id: 'funding', name: 'Funding' },
|
||||
{ id: 'issue-tracking', name: 'Issue Tracking' },
|
||||
{ id: 'license', name: 'License' },
|
||||
{ id: 'rating', name: 'Rating' },
|
||||
{ id: 'social', name: 'Social' },
|
||||
{ id: 'version', name: 'Version' },
|
||||
{
|
||||
id: 'platform-support',
|
||||
name: 'Platform & Version Support',
|
||||
keywords: ['platform'],
|
||||
},
|
||||
{ id: 'monitoring', name: 'Monitoring', keywords: ['monitoring'] },
|
||||
{ id: 'activity', name: 'Activity', keywords: ['activity'] },
|
||||
{ id: 'other', name: 'Other', keywords: [] },
|
||||
{ id: 'monitoring', name: 'Monitoring' },
|
||||
{ id: 'activity', name: 'Activity' },
|
||||
{ id: 'other', name: 'Other' },
|
||||
]
|
||||
|
@ -73,16 +73,17 @@ class LibrariesIoRepoDependencies extends LibrariesIoBase {
|
||||
pattern: ':user/:repo',
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Libraries.io dependency status for GitHub repo',
|
||||
namedParams: {
|
||||
user: 'phoenixframework',
|
||||
repo: 'phoenix',
|
||||
static openApi = {
|
||||
'/librariesio/github/{user}/{repo}': {
|
||||
get: {
|
||||
summary: 'Libraries.io dependency status for GitHub repo',
|
||||
parameters: pathParams(
|
||||
{ name: 'user', example: 'phoenixframework' },
|
||||
{ name: 'repo', example: 'phoenix' },
|
||||
),
|
||||
},
|
||||
staticPreview: renderDependenciesBadge({ outdatedCount: 325 }),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
static _cacheLength = 900
|
||||
|
||||
|
@ -1,88 +1,8 @@
|
||||
import NPMBase from '../npm/npm-base.js'
|
||||
|
||||
const keywords = ['npm']
|
||||
|
||||
export default class NodeVersionBase extends NPMBase {
|
||||
static category = 'platform-support'
|
||||
|
||||
static get examples() {
|
||||
const type = this.type
|
||||
const documentation = `
|
||||
<p>
|
||||
${this.documentation}
|
||||
The node version support is retrieved from the <code>engines.node</code> section in package.json.
|
||||
</p>
|
||||
`
|
||||
const prefix = `node-${type}`
|
||||
return [
|
||||
{
|
||||
title: `${prefix}`,
|
||||
pattern: ':packageName',
|
||||
namedParams: { packageName: 'passport' },
|
||||
staticPreview: this.renderStaticPreview({
|
||||
nodeVersionRange: '>= 6.0.0',
|
||||
}),
|
||||
keywords,
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: `${prefix} (scoped)`,
|
||||
pattern: ':scope/:packageName',
|
||||
namedParams: { scope: '@stdlib', packageName: 'stdlib' },
|
||||
staticPreview: this.renderStaticPreview({
|
||||
nodeVersionRange: '>= 6.0.0',
|
||||
}),
|
||||
keywords,
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: `${prefix} (tag)`,
|
||||
pattern: ':packageName/:tag',
|
||||
namedParams: { packageName: 'passport', tag: 'latest' },
|
||||
staticPreview: this.renderStaticPreview({
|
||||
nodeVersionRange: '>= 6.0.0',
|
||||
tag: 'latest',
|
||||
}),
|
||||
keywords,
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: `${prefix} (scoped with tag)`,
|
||||
pattern: ':scope/:packageName/:tag',
|
||||
namedParams: { scope: '@stdlib', packageName: 'stdlib', tag: 'latest' },
|
||||
staticPreview: this.renderStaticPreview({
|
||||
nodeVersionRange: '>= 6.0.0',
|
||||
tag: 'latest',
|
||||
}),
|
||||
keywords,
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: `${prefix} (scoped with tag, custom registry)`,
|
||||
pattern: ':scope/:packageName/:tag',
|
||||
namedParams: { scope: '@stdlib', packageName: 'stdlib', tag: 'latest' },
|
||||
queryParams: { registry_uri: 'https://registry.npmjs.com' },
|
||||
staticPreview: this.renderStaticPreview({
|
||||
nodeVersionRange: '>= 6.0.0',
|
||||
tag: 'latest',
|
||||
}),
|
||||
keywords,
|
||||
documentation,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
static renderStaticPreview({ tag, nodeVersionRange }) {
|
||||
// Since this badge has an async `render()` function, but `get examples()` has to
|
||||
// be synchronous, this method exists. It should return the same value as the
|
||||
// real `render()`. There's a unit test to check that.
|
||||
return {
|
||||
label: tag ? `${this.defaultBadgeData.label}@${tag}` : undefined,
|
||||
message: nodeVersionRange,
|
||||
color: 'brightgreen',
|
||||
}
|
||||
}
|
||||
|
||||
static async render({ tag, nodeVersionRange }) {
|
||||
// Atypically, the `render()` function of this badge is `async` because it needs to pull
|
||||
// data from the server.
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { pathParam, queryParam } from '../index.js'
|
||||
import { packageNameDescription } from '../npm/npm-base.js'
|
||||
import NodeVersionBase from './node-base.js'
|
||||
import { versionColorForRangeCurrent } from './node-version-color.js'
|
||||
|
||||
const description = `<p>This badge indicates whether the package supports the <b>latest</b> release of node.</p>
|
||||
<p>The node version support is retrieved from the <code>engines.node</code> section in package.json.</p>`
|
||||
|
||||
export default class NodeCurrentVersion extends NodeVersionBase {
|
||||
static route = this.buildRoute('node/v', { withTag: true })
|
||||
|
||||
@ -12,6 +17,44 @@ export default class NodeCurrentVersion extends NodeVersionBase {
|
||||
|
||||
static colorResolver = versionColorForRangeCurrent
|
||||
|
||||
static documentation =
|
||||
'This badge indicates whether the package supports the <b>latest</b> release of node'
|
||||
static openApi = {
|
||||
'/node/v/{packageName}': {
|
||||
get: {
|
||||
summary: 'Node Current',
|
||||
description,
|
||||
parameters: [
|
||||
pathParam({
|
||||
name: 'packageName',
|
||||
example: 'passport',
|
||||
description: packageNameDescription,
|
||||
}),
|
||||
queryParam({
|
||||
name: 'registry_uri',
|
||||
example: 'https://registry.npmjs.com',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
'/node/v/{packageName}/{tag}': {
|
||||
get: {
|
||||
summary: 'Node Current (with tag)',
|
||||
description,
|
||||
parameters: [
|
||||
pathParam({
|
||||
name: 'packageName',
|
||||
example: 'passport',
|
||||
description: packageNameDescription,
|
||||
}),
|
||||
pathParam({
|
||||
name: 'tag',
|
||||
example: 'latest',
|
||||
}),
|
||||
queryParam({
|
||||
name: 'registry_uri',
|
||||
example: 'https://registry.npmjs.com',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
import { test, given } from 'sazerac'
|
||||
import NodeVersion from './node-current.service.js'
|
||||
|
||||
describe('node static renderStaticPreview', function () {
|
||||
it('should have parity with render()', async function () {
|
||||
const nodeVersionRange = '>= 6.0.0'
|
||||
|
||||
const expectedNoTag = await NodeVersion.renderStaticPreview({
|
||||
nodeVersionRange,
|
||||
})
|
||||
const expectedLatestTag = await NodeVersion.renderStaticPreview({
|
||||
nodeVersionRange,
|
||||
tag: 'latest',
|
||||
})
|
||||
|
||||
test(NodeVersion.renderStaticPreview.bind(NodeVersion), () => {
|
||||
given({ nodeVersionRange }).expect(expectedNoTag)
|
||||
given({ nodeVersionRange, tag: 'latest' }).expect(expectedLatestTag)
|
||||
})
|
||||
})
|
||||
})
|
@ -1,6 +1,11 @@
|
||||
import { pathParam, queryParam } from '../index.js'
|
||||
import { packageNameDescription } from '../npm/npm-base.js'
|
||||
import NodeVersionBase from './node-base.js'
|
||||
import { versionColorForRangeLts } from './node-version-color.js'
|
||||
|
||||
const description = `<p>This badge indicates whether the package supports <b>all</b> LTS node versions.</p>
|
||||
<p>The node version support is retrieved from the <code>engines.node</code> section in package.json.</p>`
|
||||
|
||||
export default class NodeLtsVersion extends NodeVersionBase {
|
||||
static route = this.buildRoute('node/v-lts', { withTag: true })
|
||||
|
||||
@ -12,6 +17,44 @@ export default class NodeLtsVersion extends NodeVersionBase {
|
||||
|
||||
static colorResolver = versionColorForRangeLts
|
||||
|
||||
static documentation =
|
||||
'This badge indicates whether the package supports <b>all</b> LTS node versions'
|
||||
static openApi = {
|
||||
'/node/v-lts/{packageName}': {
|
||||
get: {
|
||||
summary: 'Node LTS',
|
||||
description,
|
||||
parameters: [
|
||||
pathParam({
|
||||
name: 'packageName',
|
||||
example: 'passport',
|
||||
description: packageNameDescription,
|
||||
}),
|
||||
queryParam({
|
||||
name: 'registry_uri',
|
||||
example: 'https://registry.npmjs.com',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
'/node/v-lts/{packageName}/{tag}': {
|
||||
get: {
|
||||
summary: 'Node LTS (with tag)',
|
||||
description,
|
||||
parameters: [
|
||||
pathParam({
|
||||
name: 'packageName',
|
||||
example: 'passport',
|
||||
description: packageNameDescription,
|
||||
}),
|
||||
pathParam({
|
||||
name: 'tag',
|
||||
example: 'latest',
|
||||
}),
|
||||
queryParam({
|
||||
name: 'registry_uri',
|
||||
example: 'https://registry.npmjs.com',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
import { test, given } from 'sazerac'
|
||||
import NodeVersion from './node-lts.service.js'
|
||||
|
||||
describe('node-lts renderStaticPreview', function () {
|
||||
it('should have parity with render()', async function () {
|
||||
const nodeVersionRange = '>= 6.0.0'
|
||||
|
||||
const expectedNoTag = await NodeVersion.renderStaticPreview({
|
||||
nodeVersionRange,
|
||||
})
|
||||
const expectedLatestTag = await NodeVersion.renderStaticPreview({
|
||||
nodeVersionRange,
|
||||
tag: 'latest',
|
||||
})
|
||||
|
||||
test(NodeVersion.renderStaticPreview.bind(NodeVersion), () => {
|
||||
given({ nodeVersionRange }).expect(expectedNoTag)
|
||||
given({ nodeVersionRange, tag: 'latest' }).expect(expectedLatestTag)
|
||||
})
|
||||
})
|
||||
})
|
@ -52,7 +52,6 @@ const queryParamWithFormatSchema = Joi.object({
|
||||
format: Joi.string().allow('short', 'long').optional(),
|
||||
}).required()
|
||||
|
||||
const keywords = ['sonarcloud', 'sonarqube']
|
||||
const documentation = `<p>
|
||||
The Sonar badges will work with both SonarCloud.io and self-hosted SonarQube instances.
|
||||
Just enter the correct protocol and path for your target Sonar deployment.
|
||||
@ -71,6 +70,5 @@ export {
|
||||
queryParamWithFormatSchema,
|
||||
negativeMetricColorScale,
|
||||
positiveMetricColorScale,
|
||||
keywords,
|
||||
documentation,
|
||||
}
|
||||
|
@ -58,11 +58,6 @@ export class TravisComBuild extends BaseSvgScrapingService {
|
||||
},
|
||||
}
|
||||
|
||||
static staticPreview = {
|
||||
message: 'passing',
|
||||
color: 'brightgreen',
|
||||
}
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'build',
|
||||
}
|
||||
|
@ -52,15 +52,6 @@ const statisticSchema = Joi.object().keys({
|
||||
})
|
||||
|
||||
export default class VisualStudioMarketplaceBase extends BaseJsonService {
|
||||
static keywords = [
|
||||
'vscode',
|
||||
'tfs',
|
||||
'vsts',
|
||||
'visual-studio-marketplace',
|
||||
'vs-marketplace',
|
||||
'vscode-marketplace',
|
||||
]
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'vs marketplace',
|
||||
color: 'blue',
|
||||
|
@ -93,16 +93,6 @@ class WordpressPluginTestedVersion extends BaseWordpress {
|
||||
|
||||
static defaultBadgeData = { label: 'wordpress' }
|
||||
|
||||
static renderStaticPreview({ testedVersion }) {
|
||||
// Since this badge has an async `render()` function, but `get examples()` has to
|
||||
// be synchronous, this method exists. It should return the same value as the
|
||||
// real `render()`.
|
||||
return {
|
||||
message: `${addv(testedVersion)} tested`,
|
||||
color: 'brightgreen',
|
||||
}
|
||||
}
|
||||
|
||||
static async render({ testedVersion }) {
|
||||
// Atypically, the `render()` function of this badge is `async` because it needs to pull
|
||||
// data from the server.
|
||||
|
Loading…
x
Reference in New Issue
Block a user