mirror of
https://github.com/badges/shields.git
synced 2025-04-18 19:44:04 +03:00
Export OpenAPI definitions from service examples; affects [dynamic endpoint static] (#8966)
* WIP export OpenAPI definitions from service examples * allow services to optionally define an OpenApi Paths Object instead of examples * make use of param descriptions and required query params * convert other 'core' services to declare openApi.. ..instead of examples * tweak descriptions for standard query params * move stuff around, add a high-level integration test for category2openapi * update simple-icons text refs #9054 * remove legacy param names
This commit is contained in:
parent
ba4a5619ec
commit
148b51d554
1
.gitignore
vendored
1
.gitignore
vendored
@ -96,6 +96,7 @@ typings/
|
||||
badge-examples.json
|
||||
supported-features.json
|
||||
service-definitions.yml
|
||||
frontend/categories/*.yaml
|
||||
|
||||
# Local runtime configuration.
|
||||
/config/local*.yml
|
||||
|
@ -140,6 +140,15 @@ class BaseService {
|
||||
*/
|
||||
static examples = []
|
||||
|
||||
/**
|
||||
* Optional: an OpenAPI Paths Object describing this service's
|
||||
* route or routes in OpenAPI format.
|
||||
*
|
||||
* @see https://swagger.io/specification/#paths-object
|
||||
* @abstract
|
||||
*/
|
||||
static openApi = undefined
|
||||
|
||||
static get _cacheLength() {
|
||||
const cacheLengths = {
|
||||
build: 30,
|
||||
@ -183,7 +192,7 @@ class BaseService {
|
||||
}
|
||||
|
||||
static getDefinition() {
|
||||
const { category, name, isDeprecated } = this
|
||||
const { category, name, isDeprecated, openApi } = this
|
||||
const { base, format, pattern } = this.route
|
||||
const queryParams = getQueryParamNames(this.route)
|
||||
|
||||
@ -200,7 +209,7 @@ class BaseService {
|
||||
route = undefined
|
||||
}
|
||||
|
||||
const result = { category, name, isDeprecated, route, examples }
|
||||
const result = { category, name, isDeprecated, route, examples, openApi }
|
||||
|
||||
assertValidServiceDefinition(result, `getDefinition() for ${this.name}`)
|
||||
|
||||
|
@ -129,6 +129,7 @@ function transformExample(inExample, index, ServiceClass) {
|
||||
ServiceClass
|
||||
)
|
||||
|
||||
const category = categories.find(c => c.id === ServiceClass.category)
|
||||
return {
|
||||
title,
|
||||
example: {
|
||||
@ -146,9 +147,7 @@ function transformExample(inExample, index, ServiceClass) {
|
||||
style: style === 'flat' ? undefined : style,
|
||||
namedLogo,
|
||||
},
|
||||
keywords: keywords.concat(
|
||||
categories.find(c => c.id === ServiceClass.category).keywords
|
||||
),
|
||||
keywords: category ? keywords.concat(category.keywords) : keywords,
|
||||
documentation: documentation ? { __html: documentation } : undefined,
|
||||
}
|
||||
}
|
||||
|
335
core/base-service/openapi.js
Normal file
335
core/base-service/openapi.js
Normal file
@ -0,0 +1,335 @@
|
||||
const baseUrl = process.env.BASE_URL || 'https://img.shields.io'
|
||||
const globalParamRefs = [
|
||||
{ $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' },
|
||||
]
|
||||
|
||||
function getCodeSamples(altText) {
|
||||
return [
|
||||
{
|
||||
lang: 'URL',
|
||||
label: 'URL',
|
||||
source: '$url',
|
||||
},
|
||||
{
|
||||
lang: 'Markdown',
|
||||
label: 'Markdown',
|
||||
source: ``,
|
||||
},
|
||||
{
|
||||
lang: 'reStructuredText',
|
||||
label: 'rSt',
|
||||
source: `.. image:: $url\n: alt: ${altText}`,
|
||||
},
|
||||
{
|
||||
lang: 'AsciiDoc',
|
||||
label: 'AsciiDoc',
|
||||
source: `image:$url[${altText}]`,
|
||||
},
|
||||
{
|
||||
lang: 'HTML',
|
||||
label: 'HTML',
|
||||
source: `<img alt="${altText}" src="$url">`,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
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)
|
||||
if (match === null) {
|
||||
return undefined
|
||||
}
|
||||
if (!match[1].includes('|')) {
|
||||
return undefined
|
||||
}
|
||||
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)) {
|
||||
paths[key] = endpoints[key]
|
||||
paths[key].get.parameters = [
|
||||
...paths[key].get.parameters,
|
||||
...globalParamRefs,
|
||||
]
|
||||
paths[key].get['x-code-samples'] = getCodeSamples(paths[key].get.summary)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
function services2openapi(services) {
|
||||
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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
function category2openapi(category, services) {
|
||||
const spec = {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
version: '1.0.0',
|
||||
title: category.name,
|
||||
license: {
|
||||
name: 'CC0',
|
||||
},
|
||||
},
|
||||
servers: [{ url: baseUrl }],
|
||||
components: {
|
||||
parameters: {
|
||||
style: {
|
||||
name: 'style',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'One of: flat (default), flat-square, plastic, for-the-badge, social',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'flat',
|
||||
},
|
||||
logo: {
|
||||
name: 'logo',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'One of the named logos (bitcoin, dependabot, gitlab, npm, paypal, serverfault, stackexchange, superuser, telegram, travis) or simple-icons. All simple-icons are referenced using icon slugs. You can click the icon title on <a href="https://simpleicons.org/" rel="noopener noreferrer" target="_blank">simple-icons</a> to copy the slug or they can be found in the <a href="https://github.com/simple-icons/simple-icons/blob/master/slugs.md">slugs.md file</a> in the simple-icons repository.',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'appveyor',
|
||||
},
|
||||
logoColor: {
|
||||
name: 'logoColor',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'The color of the logo (hex, rgb, rgba, hsl, hsla and css named colors supported). Supported for named logos and Shields logos but not for custom logos. For multicolor Shields logos, the corresponding named logo will be used and colored.',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'violet',
|
||||
},
|
||||
label: {
|
||||
name: 'label',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Override the default left-hand-side text (<a href="https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding">URL-Encoding</a> needed for spaces or special characters!)',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'healthiness',
|
||||
},
|
||||
labelColor: {
|
||||
name: 'labelColor',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Background color of the left part (hex, rgb, rgba, hsl, hsla and css named colors supported).',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'abcdef',
|
||||
},
|
||||
color: {
|
||||
name: 'color',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Background color of the right part (hex, rgb, rgba, hsl, hsla and css named colors supported).',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'fedcba',
|
||||
},
|
||||
cacheSeconds: {
|
||||
name: 'cacheSeconds',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'HTTP cache lifetime (rules are applied to infer a default value on a per-badge basis, any values specified below the default will be ignored).',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: '3600',
|
||||
},
|
||||
link: {
|
||||
name: 'link',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Specify what clicking on the left/right of a badge should do. Note that this only works when integrating your badge in an `<object>` HTML tag, but not an `<img>` tag or a markup language.',
|
||||
style: 'form',
|
||||
explode: true,
|
||||
schema: {
|
||||
type: 'array',
|
||||
maxItems: 2,
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
paths: services2openapi(services),
|
||||
}
|
||||
|
||||
return spec
|
||||
}
|
||||
|
||||
export { category2openapi }
|
379
core/base-service/openapi.spec.js
Normal file
379
core/base-service/openapi.spec.js
Normal file
@ -0,0 +1,379 @@
|
||||
import chai from 'chai'
|
||||
import { category2openapi } from './openapi.js'
|
||||
import BaseJsonService from './base-json.js'
|
||||
const { expect } = chai
|
||||
|
||||
class OpenApiService extends BaseJsonService {
|
||||
static category = 'build'
|
||||
static route = { base: 'openapi/service', pattern: ':packageName/:distTag*' }
|
||||
|
||||
// this service defines its own API Paths Object
|
||||
static openApi = {
|
||||
'/openapi/service/{packageName}': {
|
||||
get: {
|
||||
summary: 'OpenApiService Summary',
|
||||
description: 'OpenApiService Description',
|
||||
parameters: [
|
||||
{
|
||||
name: 'packageName',
|
||||
description: 'packageName description',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'badge-maker',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'/openapi/service/{packageName}/{distTag}': {
|
||||
get: {
|
||||
summary: 'OpenApiService Summary (with Tag)',
|
||||
description: 'OpenApiService Description (with Tag)',
|
||||
parameters: [
|
||||
{
|
||||
name: 'packageName',
|
||||
description: 'packageName description',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'badge-maker',
|
||||
},
|
||||
{
|
||||
name: 'distTag',
|
||||
description: 'distTag description',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'latest',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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' } },
|
||||
servers: [{ url: 'https://img.shields.io' }],
|
||||
components: {
|
||||
parameters: {
|
||||
style: {
|
||||
name: 'style',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'One of: flat (default), flat-square, plastic, for-the-badge, social',
|
||||
schema: { type: 'string' },
|
||||
example: 'flat',
|
||||
},
|
||||
logo: {
|
||||
name: 'logo',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'One of the named logos (bitcoin, dependabot, gitlab, npm, paypal, serverfault, stackexchange, superuser, telegram, travis) or simple-icons. All simple-icons are referenced using icon slugs. You can click the icon title on <a href="https://simpleicons.org/" rel="noopener noreferrer" target="_blank">simple-icons</a> to copy the slug or they can be found in the <a href="https://github.com/simple-icons/simple-icons/blob/master/slugs.md">slugs.md file</a> in the simple-icons repository.',
|
||||
schema: { type: 'string' },
|
||||
example: 'appveyor',
|
||||
},
|
||||
logoColor: {
|
||||
name: 'logoColor',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'The color of the logo (hex, rgb, rgba, hsl, hsla and css named colors supported). Supported for named logos and Shields logos but not for custom logos. For multicolor Shields logos, the corresponding named logo will be used and colored.',
|
||||
schema: { type: 'string' },
|
||||
example: 'violet',
|
||||
},
|
||||
label: {
|
||||
name: 'label',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Override the default left-hand-side text (<a href="https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding">URL-Encoding</a> needed for spaces or special characters!)',
|
||||
schema: { type: 'string' },
|
||||
example: 'healthiness',
|
||||
},
|
||||
labelColor: {
|
||||
name: 'labelColor',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Background color of the left part (hex, rgb, rgba, hsl, hsla and css named colors supported).',
|
||||
schema: { type: 'string' },
|
||||
example: 'abcdef',
|
||||
},
|
||||
color: {
|
||||
name: 'color',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Background color of the right part (hex, rgb, rgba, hsl, hsla and css named colors supported).',
|
||||
schema: { type: 'string' },
|
||||
example: 'fedcba',
|
||||
},
|
||||
cacheSeconds: {
|
||||
name: 'cacheSeconds',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'HTTP cache lifetime (rules are applied to infer a default value on a per-badge basis, any values specified below the default will be ignored).',
|
||||
schema: { type: 'string' },
|
||||
example: '3600',
|
||||
},
|
||||
link: {
|
||||
name: 'link',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Specify what clicking on the left/right of a badge should do. Note that this only works when integrating your badge in an `<object>` HTML tag, but not an `<img>` tag or a markup language.',
|
||||
style: 'form',
|
||||
explode: true,
|
||||
schema: { type: 'array', maxItems: 2, items: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
paths: {
|
||||
'/openapi/service/{packageName}': {
|
||||
get: {
|
||||
summary: 'OpenApiService Summary',
|
||||
description: 'OpenApiService Description',
|
||||
parameters: [
|
||||
{
|
||||
name: 'packageName',
|
||||
description: 'packageName description',
|
||||
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: OpenApiService Summary',
|
||||
},
|
||||
{
|
||||
lang: 'AsciiDoc',
|
||||
label: 'AsciiDoc',
|
||||
source: 'image:$url[OpenApiService Summary]',
|
||||
},
|
||||
{
|
||||
lang: 'HTML',
|
||||
label: 'HTML',
|
||||
source: '<img alt="OpenApiService Summary" src="$url">',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'/openapi/service/{packageName}/{distTag}': {
|
||||
get: {
|
||||
summary: 'OpenApiService Summary (with Tag)',
|
||||
description: 'OpenApiService Description (with Tag)',
|
||||
parameters: [
|
||||
{
|
||||
name: 'packageName',
|
||||
description: 'packageName description',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'badge-maker',
|
||||
},
|
||||
{
|
||||
name: 'distTag',
|
||||
description: 'distTag description',
|
||||
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: OpenApiService Summary (with Tag)',
|
||||
},
|
||||
{
|
||||
lang: 'AsciiDoc',
|
||||
label: 'AsciiDoc',
|
||||
source: 'image:$url[OpenApiService Summary (with Tag)]',
|
||||
},
|
||||
{
|
||||
lang: 'HTML',
|
||||
label: 'HTML',
|
||||
source: '<img alt="OpenApiService Summary (with Tag)" src="$url">',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'/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">',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
function clean(obj) {
|
||||
// remove any undefined values in the object
|
||||
return JSON.parse(JSON.stringify(obj))
|
||||
}
|
||||
|
||||
describe('category2openapi', function () {
|
||||
it('generates an Open API spec', function () {
|
||||
expect(
|
||||
clean(
|
||||
category2openapi({ name: 'build' }, [
|
||||
OpenApiService.getDefinition(),
|
||||
LegacyService.getDefinition(),
|
||||
])
|
||||
)
|
||||
).to.deep.equal(expected)
|
||||
})
|
||||
})
|
@ -46,6 +46,28 @@ const serviceDefinition = Joi.object({
|
||||
})
|
||||
)
|
||||
.default([]),
|
||||
openApi: Joi.object().pattern(
|
||||
/./,
|
||||
Joi.object({
|
||||
get: Joi.object({
|
||||
summary: Joi.string().required(),
|
||||
description: Joi.string().required(),
|
||||
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() }).required(),
|
||||
example: Joi.string(),
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
.required(),
|
||||
}).required(),
|
||||
}).required()
|
||||
),
|
||||
}).required()
|
||||
|
||||
function assertValidServiceDefinition(example, message = undefined) {
|
||||
|
0
frontend/categories/.gitkeep
Normal file
0
frontend/categories/.gitkeep
Normal file
49
scripts/export-openapi-cli.js
Normal file
49
scripts/export-openapi-cli.js
Normal file
@ -0,0 +1,49 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import yaml from 'js-yaml'
|
||||
import { collectDefinitions } from '../core/base-service/loader.js'
|
||||
import { category2openapi } from '../core/base-service/openapi.js'
|
||||
|
||||
const specsPath = path.join('frontend', 'categories')
|
||||
|
||||
function writeSpec(filename, spec) {
|
||||
// Omit undefined
|
||||
// https://github.com/nodeca/js-yaml/issues/356#issuecomment-312430599
|
||||
const cleaned = JSON.parse(JSON.stringify(spec))
|
||||
|
||||
fs.writeFileSync(
|
||||
filename,
|
||||
yaml.dump(cleaned, { flowLevel: 5, forceQuotes: true })
|
||||
)
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
const definitions = await collectDefinitions()
|
||||
|
||||
for (const category of definitions.categories) {
|
||||
const services = definitions.services.filter(
|
||||
service => service.category === category.id && !service.isDeprecated
|
||||
)
|
||||
|
||||
writeSpec(
|
||||
path.join(specsPath, `${category.id}.yaml`),
|
||||
category2openapi(category, services)
|
||||
)
|
||||
}
|
||||
|
||||
let coreServices = []
|
||||
coreServices = coreServices.concat(
|
||||
definitions.services.filter(
|
||||
service => service.category === 'static' && !service.isDeprecated
|
||||
)
|
||||
)
|
||||
coreServices = coreServices.concat(
|
||||
definitions.services.filter(
|
||||
service => service.category === 'dynamic' && !service.isDeprecated
|
||||
)
|
||||
)
|
||||
writeSpec(
|
||||
path.join(specsPath, '1core.yaml'),
|
||||
category2openapi({ name: 'Core' }, coreServices)
|
||||
)
|
||||
})()
|
@ -3,6 +3,19 @@ import { collectDefinitions } from '../core/base-service/loader.js'
|
||||
;(async () => {
|
||||
const definitions = await collectDefinitions()
|
||||
|
||||
// filter out static, dynamic and debug badge examples
|
||||
const publicCategories = definitions.categories.map(c => c.id)
|
||||
definitions.services = definitions.services.filter(s =>
|
||||
publicCategories.includes(s.category)
|
||||
)
|
||||
|
||||
// drop the openApi property for the "legacy" frontend
|
||||
for (const service of definitions.services) {
|
||||
if (service.openApi) {
|
||||
service.openApi = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// Omit undefined
|
||||
// https://github.com/nodeca/js-yaml/issues/356#issuecomment-312430599
|
||||
const cleaned = JSON.parse(JSON.stringify(definitions))
|
||||
|
@ -6,6 +6,53 @@ import jsonPath from './json-path.js'
|
||||
export default class DynamicJson extends jsonPath(BaseJsonService) {
|
||||
static enabledMetrics = [MetricNames.SERVICE_RESPONSE_SIZE]
|
||||
static route = createRoute('json')
|
||||
static openApi = {
|
||||
'/badge/dynamic/json': {
|
||||
get: {
|
||||
summary: 'Dynamic JSON Badge',
|
||||
description: `<p>
|
||||
The Dynamic JSON Badge allows you to extract an arbitrary value from any
|
||||
JSON Document using a JSONPath selector and show it on a badge.
|
||||
</p>`,
|
||||
parameters: [
|
||||
{
|
||||
name: 'url',
|
||||
description: 'The URL to a JSON document',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example:
|
||||
'https://github.com/badges/shields/raw/master/package.json',
|
||||
},
|
||||
{
|
||||
name: 'query',
|
||||
description:
|
||||
'A <a href="https://jsonpath.com/">JSONPath</a> expression that will be used to query the document',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: '$.name',
|
||||
},
|
||||
{
|
||||
name: 'prefix',
|
||||
description: 'Optional prefix to append to the value',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: '[',
|
||||
},
|
||||
{
|
||||
name: 'suffix',
|
||||
description: 'Optional suffix to append to the value',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: ']',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
async fetch({ schema, url, errorMessages }) {
|
||||
return this._requestJson({
|
||||
|
@ -15,6 +15,53 @@ export default class DynamicXml extends BaseService {
|
||||
static category = 'dynamic'
|
||||
static enabledMetrics = [MetricNames.SERVICE_RESPONSE_SIZE]
|
||||
static route = createRoute('xml')
|
||||
static openApi = {
|
||||
'/badge/dynamic/xml': {
|
||||
get: {
|
||||
summary: 'Dynamic XML Badge',
|
||||
description: `<p>
|
||||
The Dynamic XML Badge allows you to extract an arbitrary value from any
|
||||
XML Document using an XPath selector and show it on a badge.
|
||||
</p>`,
|
||||
parameters: [
|
||||
{
|
||||
name: 'url',
|
||||
description: 'The URL to a XML document',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'https://httpbin.org/xml',
|
||||
},
|
||||
{
|
||||
name: 'query',
|
||||
description:
|
||||
'A <a href="http://xpather.com/">XPath</a> expression that will be used to query the document',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: '//slideshow/slide[1]/title',
|
||||
},
|
||||
{
|
||||
name: 'prefix',
|
||||
description: 'Optional prefix to append to the value',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: '[',
|
||||
},
|
||||
{
|
||||
name: 'suffix',
|
||||
description: 'Optional suffix to append to the value',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: ']',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = { label: 'custom badge' }
|
||||
|
||||
transform({ pathExpression, buffer }) {
|
||||
|
@ -6,6 +6,53 @@ import jsonPath from './json-path.js'
|
||||
export default class DynamicYaml extends jsonPath(BaseYamlService) {
|
||||
static enabledMetrics = [MetricNames.SERVICE_RESPONSE_SIZE]
|
||||
static route = createRoute('yaml')
|
||||
static openApi = {
|
||||
'/badge/dynamic/yaml': {
|
||||
get: {
|
||||
summary: 'Dynamic YAML Badge',
|
||||
description: `<p>
|
||||
The Dynamic YAML Badge allows you to extract an arbitrary value from any
|
||||
YAML Document using a JSONPath selector and show it on a badge.
|
||||
</p>`,
|
||||
parameters: [
|
||||
{
|
||||
name: 'url',
|
||||
description: 'The URL to a YAML document',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example:
|
||||
'https://raw.githubusercontent.com/badges/shields/master/.github/dependabot.yml',
|
||||
},
|
||||
{
|
||||
name: 'query',
|
||||
description:
|
||||
'A <a href="https://jsonpath.com/">JSONPath</a> expression that will be used to query the document',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: '$.version',
|
||||
},
|
||||
{
|
||||
name: 'prefix',
|
||||
description: 'Optional prefix to append to the value',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: '[',
|
||||
},
|
||||
{
|
||||
name: 'suffix',
|
||||
description: 'Optional suffix to append to the value',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: ']',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
async fetch({ schema, url, errorMessages }) {
|
||||
return this._requestYaml({
|
||||
|
@ -11,14 +11,144 @@ const queryParamSchema = Joi.object({
|
||||
url: optionalUrl.required(),
|
||||
}).required()
|
||||
|
||||
const description = `<p>
|
||||
Using the endpoint badge, you can provide content for a badge through
|
||||
a JSON endpoint. The content can be prerendered, or generated on the
|
||||
fly. To strike a balance between responsiveness and bandwidth
|
||||
utilization on one hand, and freshness on the other, cache behavior is
|
||||
configurable, subject to the Shields minimum. The endpoint URL is
|
||||
provided to Shields through the query string. Shields fetches it and
|
||||
formats the badge.
|
||||
</p>
|
||||
<p>
|
||||
The endpoint badge takes a single required query param: <code>url</code>, which is the URL to your JSON endpoint
|
||||
</p>
|
||||
<div>
|
||||
<h2>Example JSON Endpoint Response</h2>
|
||||
<code>{ "schemaVersion": 1, "label": "hello", "message": "sweet world", "color": "orange" }</code>
|
||||
<h2>Example Shields Response</h2>
|
||||
<img src="https://img.shields.io/badge/hello-sweet_world-orange" />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Schema</h2>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Property</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>schemaVersion</code></td>
|
||||
<td>Required. Always the number <code>1</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>label</code></td>
|
||||
<td>
|
||||
Required. The left text, or the empty string to omit the left side of
|
||||
the badge. This can be overridden by the query string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>message</code></td>
|
||||
<td>Required. Can't be empty. The right text.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>color</code></td>
|
||||
<td>
|
||||
Default: <code>lightgrey</code>. The right color. Supports the eight
|
||||
named colors above, as well as hex, rgb, rgba, hsl, hsla and css named
|
||||
colors. This can be overridden by the query string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>labelColor</code></td>
|
||||
<td>
|
||||
Default: <code>grey</code>. The left color. This can be overridden by
|
||||
the query string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>isError</code></td>
|
||||
<td>
|
||||
Default: <code>false</code>. <code>true</code> to treat this as an
|
||||
error badge. This prevents the user from overriding the color. In the
|
||||
future, it may affect cache behavior.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>namedLogo</code></td>
|
||||
<td>
|
||||
Default: none. One of the named logos supported by Shields or
|
||||
<a href="https://simpleicons.org/">simple-icons</a>. Can be overridden
|
||||
by the query string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>logoSvg</code></td>
|
||||
<td>Default: none. An SVG string containing a custom logo.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>logoColor</code></td>
|
||||
<td>
|
||||
Default: none. Same meaning as the query string. Can be overridden by
|
||||
the query string. Only works for named logos and Shields logos. If you
|
||||
override the color of a multicolor Shield logo, the corresponding
|
||||
named logo will be used and colored.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>logoWidth</code></td>
|
||||
<td>
|
||||
Default: none. Same meaning as the query string. Can be overridden by
|
||||
the query string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>logoPosition</code></td>
|
||||
<td>
|
||||
Default: none. Same meaning as the query string. Can be overridden by
|
||||
the query string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>style</code></td>
|
||||
<td>
|
||||
Default: <code>flat</code>. The default template to use. Can be
|
||||
overridden by the query string.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`
|
||||
|
||||
export default class Endpoint extends BaseJsonService {
|
||||
static category = 'dynamic'
|
||||
|
||||
static route = {
|
||||
base: 'endpoint',
|
||||
pattern: '',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
static openApi = {
|
||||
'/endpoint': {
|
||||
get: {
|
||||
summary: 'Endpoint Badge',
|
||||
description,
|
||||
parameters: [
|
||||
{
|
||||
name: 'url',
|
||||
description: 'The URL to your JSON endpoint',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'https://shields.redsparr0w.com/2473/monday',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static _cacheLength = 300
|
||||
static defaultBadgeData = { label: 'custom badge' }
|
||||
|
||||
|
@ -1,15 +1,73 @@
|
||||
import { escapeFormat } from '../../core/badge-urls/path-helpers.js'
|
||||
import { BaseStaticService } from '../index.js'
|
||||
|
||||
const description = `<p>
|
||||
The static badge accepts a single required path parameter which encodes either:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Label, message and color seperated by a dash <code>-</code>. For example:<br />
|
||||
<img alt="any text: you like" src="https://img.shields.io/badge/any_text-you_like-blue" /> -
|
||||
<a href="https://img.shields.io/badge/any_text-you_like-blue">https://img.shields.io/badge/any_text-you_like-blue</a>
|
||||
</li>
|
||||
<li>
|
||||
Message and color only, seperated by a dash <code>-</code>. For example:<br />
|
||||
<img alt="just the message" src="https://img.shields.io/badge/just%20the%20message-8A2BE2" /> -
|
||||
<a href="https://img.shields.io/badge/just%20the%20message-8A2BE2">https://img.shields.io/badge/just%20the%20message-8A2BE2</a>
|
||||
</li>
|
||||
</ul>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>URL input</th>
|
||||
<th>Badge output</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Underscore <code>_</code> or <code>%20</code></td>
|
||||
<td>Space <code> </code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Double underscore <code>__</code></td>
|
||||
<td>Underscore <code>_</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Double dash <code>--</code></td>
|
||||
<td>Dash <code>-</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
Hex, rgb, rgba, hsl, hsla and css named colors may be used.
|
||||
</p>`
|
||||
|
||||
export default class StaticBadge extends BaseStaticService {
|
||||
static category = 'static'
|
||||
|
||||
static route = {
|
||||
base: '',
|
||||
format: '(?::|badge/)((?:[^-]|--)*?)-?((?:[^-]|--)*)-((?:[^-.]|--)+)',
|
||||
capture: ['label', 'message', 'color'],
|
||||
}
|
||||
|
||||
static openApi = {
|
||||
'/badge/{badgeContent}': {
|
||||
get: {
|
||||
summary: 'Static Badge',
|
||||
description,
|
||||
parameters: [
|
||||
{
|
||||
name: 'badgeContent',
|
||||
description:
|
||||
'Label, (optional) message, and color. Seperated by dashes',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'build-passing-brightgreen',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
handle({ label, message, color }) {
|
||||
return { label: escapeFormat(label), message: escapeFormat(message), color }
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user