1
0
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:
chris48s 2024-02-24 18:14:44 +00:00 committed by GitHub
parent 8ab9dfa9a1
commit 9cfd301b82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 194 additions and 941 deletions

View File

@ -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

View File

@ -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)
})
})

View File

@ -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() {

View File

@ -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({

View File

@ -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 }

View File

@ -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,
})
})

View File

@ -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, '&#123;')
.replace(/}/g, '&#125;')
.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

View File

@ -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: '![LegacyService Title (with Tag)]($url)',
},
{
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: '![LegacyService Title (with Tag)]($url)',
},
{
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)

View File

@ -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 }) {

View File

@ -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 () {

View File

@ -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 {

View File

@ -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

View File

@ -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' },
]

View File

@ -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

View File

@ -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.

View File

@ -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',
}),
],
},
},
}
}

View File

@ -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)
})
})
})

View File

@ -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',
}),
],
},
},
}
}

View File

@ -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)
})
})
})

View File

@ -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,
}

View File

@ -58,11 +58,6 @@ export class TravisComBuild extends BaseSvgScrapingService {
},
}
static staticPreview = {
message: 'passing',
color: 'brightgreen',
}
static defaultBadgeData = {
label: 'build',
}

View File

@ -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',

View File

@ -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.