diff --git a/.gitignore b/.gitignore
index a645da247b..5c8b82a5b4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -96,6 +96,7 @@ typings/
badge-examples.json
supported-features.json
service-definitions.yml
+frontend/categories/*.yaml
# Local runtime configuration.
/config/local*.yml
diff --git a/core/base-service/base.js b/core/base-service/base.js
index 4befd03cc2..f60470fa3b 100644
--- a/core/base-service/base.js
+++ b/core/base-service/base.js
@@ -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}`)
diff --git a/core/base-service/examples.js b/core/base-service/examples.js
index 6aa9ff03ed..4f7f2342df 100644
--- a/core/base-service/examples.js
+++ b/core/base-service/examples.js
@@ -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,
}
}
diff --git a/core/base-service/openapi.js b/core/base-service/openapi.js
new file mode 100644
index 0000000000..1cb463a76c
--- /dev/null
+++ b/core/base-service/openapi.js
@@ -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: `
`,
+ },
+ ]
+}
+
+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(/