/** * Functions for publishing the shields.io URL schema as an OpenAPI Document * * @module */ const baseUrl = process.env.BASE_URL const globalParamRefs = [ { $ref: '#/components/parameters/style' }, { $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: `![${altText}]($url)`, }, { lang: 'reStructuredText', label: 'rSt', source: `.. image:: $url\n :alt: ${altText}`, }, { lang: 'AsciiDoc', label: 'AsciiDoc', source: `image:$url[${altText}]`, }, { lang: 'HTML', label: 'HTML', source: `${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) 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(/
/g, '
') // react does not like
.replace(/{/g, '{') .replace(/}/g, '}') .replace(/