Convert examples arrays to openApi objects (part 1) (#9320)
* add helper functions for generating Open API path/query params with defaults * tweak Open API schema - make description optional - allow null example + allowEmptyValue (for boolean query params) * convert examples --> openApi in amo * convert examples --> openApi in ansible * convert examples --> openApi in appveyor build/job * add re-usable Open API query param for test-results badges we can use these for all the 'test results' badges * convert examples --> openApi in appveyor tests * DRY up existing dynamic/endpoint param definitions * DRY up queryParam * allow enum param in serviceDefinition schema * improve misleading param name * check route and openApi are consistent on service load * fix mistake in ansible role route * documentation --> description * add pathParams and queryParams helpers +docstrings * give everything a search-friendly summary, check for duplicate summary * prettier fixup
This commit is contained in:
@@ -189,6 +189,22 @@ class BaseService {
|
||||
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}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static getDefinition() {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Deprecated,
|
||||
ImproperlyConfigured,
|
||||
} from './errors.js'
|
||||
import { pathParam, pathParams, queryParam, queryParams } from './openapi.js'
|
||||
|
||||
export {
|
||||
BaseService,
|
||||
@@ -32,4 +33,8 @@ export {
|
||||
InvalidParameter,
|
||||
ImproperlyConfigured,
|
||||
Deprecated,
|
||||
pathParam,
|
||||
pathParams,
|
||||
queryParam,
|
||||
queryParams,
|
||||
}
|
||||
|
||||
@@ -83,6 +83,18 @@ async function loadServiceClasses(servicePaths) {
|
||||
},
|
||||
)
|
||||
|
||||
const routeSummaries = []
|
||||
serviceClasses.forEach(function (serviceClass) {
|
||||
if (serviceClass.openApi) {
|
||||
for (const route of Object.values(serviceClass.openApi)) {
|
||||
routeSummaries.push(route.get.summary)
|
||||
}
|
||||
}
|
||||
})
|
||||
assertNamesUnique(routeSummaries, {
|
||||
message: 'Duplicate route summary found',
|
||||
})
|
||||
|
||||
return serviceClasses
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* 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' },
|
||||
@@ -332,4 +338,132 @@ function category2openapi(category, services) {
|
||||
return spec
|
||||
}
|
||||
|
||||
export { category2openapi }
|
||||
/**
|
||||
* Helper function for assembling an OpenAPI path parameter object
|
||||
*
|
||||
* @param {module:core/base-service/openapi~PathParamInput} param Input param
|
||||
* @returns {module:core/base-service/openapi~OpenApiParam} OpenAPI Parameter Object
|
||||
* @see https://swagger.io/specification/#parameter-object
|
||||
*/
|
||||
function pathParam({
|
||||
name,
|
||||
example,
|
||||
schema = { type: 'string' },
|
||||
description,
|
||||
}) {
|
||||
return { name, in: 'path', required: true, schema, example, description }
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for assembling an array of OpenAPI path parameter objects
|
||||
* The code
|
||||
* ```
|
||||
* const params = pathParams(
|
||||
* { name: 'name1', example: 'example1' },
|
||||
* { name: 'name2', example: 'example2' },
|
||||
* )
|
||||
* ```
|
||||
* is equivilent to
|
||||
* ```
|
||||
* const params = [
|
||||
* pathParam({ name: 'name1', example: 'example1' }),
|
||||
* pathParam({ name: 'name2', example: 'example2' }),
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* @param {...module:core/base-service/openapi~PathParamInput} params Input params
|
||||
* @returns {Array.<module:core/base-service/openapi~OpenApiParam>} Array of OpenAPI Parameter Objects
|
||||
* @see {@link module:core/base-service/openapi~pathParam}
|
||||
*/
|
||||
function pathParams(...params) {
|
||||
return params.map(param => pathParam(param))
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for assembling an OpenAPI query parameter object
|
||||
*
|
||||
* @param {module:core/base-service/openapi~QueryParamInput} param Input param
|
||||
* @returns {module:core/base-service/openapi~OpenApiParam} OpenAPI Parameter Object
|
||||
* @see https://swagger.io/specification/#parameter-object
|
||||
*/
|
||||
function queryParam({
|
||||
name,
|
||||
example,
|
||||
schema = { type: 'string' },
|
||||
required = false,
|
||||
description,
|
||||
}) {
|
||||
const param = { name, in: 'query', required, schema, example, description }
|
||||
if (example === null && schema.type === 'boolean') {
|
||||
param.allowEmptyValue = true
|
||||
}
|
||||
return param
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for assembling an array of OpenAPI query parameter objects
|
||||
* The code
|
||||
* ```
|
||||
* const params = queryParams(
|
||||
* { name: 'name1', example: 'example1' },
|
||||
* { name: 'name2', example: 'example2' },
|
||||
* )
|
||||
* ```
|
||||
* is equivilent to
|
||||
* ```
|
||||
* const params = [
|
||||
* queryParam({ name: 'name1', example: 'example1' }),
|
||||
* queryParams({ name: 'name2', example: 'example2' }),
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* @param {...module:core/base-service/openapi~QueryParamInput} params Input params
|
||||
* @returns {Array.<module:core/base-service/openapi~OpenApiParam>} Array of OpenAPI Parameter Objects
|
||||
* @see {@link module:core/base-service/openapi~queryParam}
|
||||
*/
|
||||
function queryParams(...params) {
|
||||
return params.map(param => queryParam(param))
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} PathParamInput
|
||||
* @property {string} name The name of the parameter. Parameter names are case sensitive
|
||||
* @property {string} example Example of a valid value for this parameter
|
||||
* @property {object} [schema={ type: 'string' }] Parameter schema.
|
||||
* An [OpenAPI Schema object](https://swagger.io/specification/#schema-object)
|
||||
* specifying the parameter type.
|
||||
* Normally this should be omitted as all path parameters are strings.
|
||||
* Use this when we also want to pass an enum of valid parameters
|
||||
* to be presented as a drop-down in the frontend. e.g:
|
||||
* `{'type': 'string', 'enum': ['github', 'bitbucket'}` (Optional)
|
||||
* @property {string} description A brief description of the parameter (Optional)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} QueryParamInput
|
||||
* @property {string} name The name of the parameter. Parameter names are case sensitive
|
||||
* @property {string|null} example Example of a valid value for this parameter
|
||||
* @property {object} [schema={ type: 'string' }] Parameter schema.
|
||||
* An [OpenAPI Schema object](https://swagger.io/specification/#schema-object)
|
||||
* specifying the parameter type. This can normally be omitted.
|
||||
* Query params are usually strings. (Optional)
|
||||
* @property {boolean} [required=false] Determines whether this parameter is mandatory (Optional)
|
||||
* @property {string} description A brief description of the parameter (Optional)
|
||||
*/
|
||||
|
||||
/**
|
||||
* OpenAPI Parameter Object
|
||||
*
|
||||
* @typedef {object} OpenApiParam
|
||||
* @property {string} name The name of the parameter
|
||||
* @property {string|null} example Example of a valid value for this parameter
|
||||
* @property {('path'|'query')} in The location of the parameter
|
||||
* @property {object} schema Parameter schema.
|
||||
* An [OpenAPI Schema object](https://swagger.io/specification/#schema-object)
|
||||
* specifying the parameter type.
|
||||
* @property {boolean} required Determines whether this parameter is mandatory
|
||||
* @property {string} description A brief description of the parameter
|
||||
* @property {boolean} allowEmptyValue If true, allows the ability to pass an empty value to this parameter
|
||||
*/
|
||||
|
||||
export { category2openapi, pathParam, pathParams, queryParam, queryParams }
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import chai from 'chai'
|
||||
import { category2openapi } from './openapi.js'
|
||||
import {
|
||||
category2openapi,
|
||||
pathParam,
|
||||
pathParams,
|
||||
queryParam,
|
||||
queryParams,
|
||||
} from './openapi.js'
|
||||
import BaseJsonService from './base-json.js'
|
||||
const { expect } = chai
|
||||
|
||||
@@ -376,3 +382,148 @@ describe('category2openapi', function () {
|
||||
).to.deep.equal(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('pathParam, pathParams', function () {
|
||||
it('generates a pathParam with defaults', function () {
|
||||
const input = { name: 'name', example: 'example' }
|
||||
const expected = {
|
||||
name: 'name',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'example',
|
||||
description: undefined,
|
||||
}
|
||||
expect(pathParam(input)).to.deep.equal(expected)
|
||||
expect(pathParams(input)[0]).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('generates a pathParam with custom args', function () {
|
||||
const input = {
|
||||
name: 'name',
|
||||
example: true,
|
||||
schema: { type: 'boolean' },
|
||||
description: 'long desc',
|
||||
}
|
||||
const expected = {
|
||||
name: 'name',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: {
|
||||
type: 'boolean',
|
||||
},
|
||||
example: true,
|
||||
description: 'long desc',
|
||||
}
|
||||
expect(pathParam(input)).to.deep.equal(expected)
|
||||
expect(pathParams(input)[0]).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('generates multiple pathParams', function () {
|
||||
expect(
|
||||
pathParams(
|
||||
{ name: 'name1', example: 'example1' },
|
||||
{ name: 'name2', example: 'example2' },
|
||||
),
|
||||
).to.deep.equal([
|
||||
{
|
||||
name: 'name1',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'example1',
|
||||
description: undefined,
|
||||
},
|
||||
{
|
||||
name: 'name2',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'example2',
|
||||
description: undefined,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('queryParam, queryParams', function () {
|
||||
it('generates a queryParam with defaults', function () {
|
||||
const input = { name: 'name', example: 'example' }
|
||||
const expected = {
|
||||
name: 'name',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: 'example',
|
||||
description: undefined,
|
||||
}
|
||||
expect(queryParam(input)).to.deep.equal(expected)
|
||||
expect(queryParams(input)[0]).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('generates queryParam with custom args', function () {
|
||||
const input = {
|
||||
name: 'name',
|
||||
example: 'example',
|
||||
required: true,
|
||||
description: 'long desc',
|
||||
}
|
||||
const expected = {
|
||||
name: 'name',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'example',
|
||||
description: 'long desc',
|
||||
}
|
||||
expect(queryParam(input)).to.deep.equal(expected)
|
||||
expect(queryParams(input)[0]).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('generates a queryParam with boolean/null example', function () {
|
||||
const input = { name: 'name', example: null, schema: { type: 'boolean' } }
|
||||
const expected = {
|
||||
name: 'name',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'boolean' },
|
||||
allowEmptyValue: true,
|
||||
example: null,
|
||||
description: undefined,
|
||||
}
|
||||
expect(queryParam(input)).to.deep.equal(expected)
|
||||
expect(queryParams(input)[0]).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('generates multiple queryParams', function () {
|
||||
expect(
|
||||
queryParams(
|
||||
{ name: 'name1', example: 'example1' },
|
||||
{ name: 'name2', example: 'example2' },
|
||||
),
|
||||
).to.deep.equal([
|
||||
{
|
||||
name: 'name1',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: 'example1',
|
||||
description: undefined,
|
||||
},
|
||||
{
|
||||
name: 'name2',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: 'example2',
|
||||
description: undefined,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -48,7 +48,7 @@ const serviceDefinition = Joi.object({
|
||||
Joi.object({
|
||||
get: Joi.object({
|
||||
summary: Joi.string().required(),
|
||||
description: Joi.string().required(),
|
||||
description: Joi.string(),
|
||||
parameters: Joi.array()
|
||||
.items(
|
||||
Joi.object({
|
||||
@@ -56,8 +56,12 @@ const serviceDefinition = Joi.object({
|
||||
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(),
|
||||
schema: Joi.object({
|
||||
type: Joi.string().required(),
|
||||
enum: Joi.array(),
|
||||
}).required(),
|
||||
allowEmptyValue: Joi.boolean(),
|
||||
example: Joi.string().allow(null),
|
||||
}),
|
||||
)
|
||||
.min(1)
|
||||
@@ -67,8 +71,8 @@ const serviceDefinition = Joi.object({
|
||||
),
|
||||
}).required()
|
||||
|
||||
function assertValidServiceDefinition(example, message = undefined) {
|
||||
Joi.assert(example, serviceDefinition, message)
|
||||
function assertValidServiceDefinition(service, message = undefined) {
|
||||
Joi.assert(service, serviceDefinition, message)
|
||||
}
|
||||
|
||||
const serviceDefinitionExport = Joi.object({
|
||||
|
||||
Reference in New Issue
Block a user