in compiler/src/steps/validate-rest-spec.ts [41:215]
export default async function validateRestSpec (model: model.Model, jsonSpec: Map<string, JsonSpec>, errors: ValidationErrors): Promise<model.Model> {
for (const endpoint of model.endpoints) {
if (endpoint.request == null) continue
const requestDefinition = getDefinition(endpoint.request)
const requestProperties = getProperties(requestDefinition)
if (endpoint.request.name === LOG || LOG === 'ALL') {
const spec = jsonSpec.get(endpoint.name)
assert(spec, `Can't find the json spec for ${endpoint.name}`)
// Check URL paths and methods
if (spec.url.paths.length !== endpoint.urls.length) {
errors.addEndpointError(endpoint.name, 'request', `${endpoint.request.name}: different number of urls in the json spec`)
} else {
for (const modelUrl of endpoint.urls) {
// URL path
const restSpecUrl = spec.url.paths.find(path => path.path === modelUrl.path)
if (restSpecUrl == null) {
errors.addEndpointError(endpoint.name, 'request', `${endpoint.request.name}: url path '${modelUrl.path}' not found in the json spec`)
} else {
// URL methods
if (!deepEqual([...restSpecUrl.methods].sort(), [...modelUrl.methods].sort())) {
errors.addEndpointError(endpoint.name, 'request', `${modelUrl.path}: different http methods in the json spec`)
}
// Deprecation.
if ((restSpecUrl.deprecated != null) !== (modelUrl.deprecation != null)) {
errors.addEndpointError(endpoint.name, 'request', `${endpoint.request.name}: different deprecation in the json spec`)
}
}
}
}
// Check url parts
const urlParts = Array.from(new Set(spec.url.paths
.filter(path => path.parts != null)
.flatMap(path => {
assert(path.parts != null)
return Object.keys(path.parts)
})
))
const pathProperties = requestProperties.path.map(property => property.name)
// are all the parameters in the request definition present in the json spec?
for (const name of pathProperties) {
if (!urlParts.includes(name)) {
errors.addEndpointError(endpoint.name, 'request', `${endpoint.request.name}: path parameter '${name}' does not exist in the json spec`)
}
}
// are all the parameters in the json spec present in the request definition?
for (const name of urlParts) {
if (!pathProperties.includes(name)) {
errors.addEndpointError(endpoint.name, 'request', `${endpoint.request.name}: missing json spec path parameter '${name}'`)
}
}
// are all path parameters properly required or optional?
let urlPartsRequired = new Set(urlParts)
// A part is considered required if it is included in
// every path for the API endpoint.
for (const path of spec.url.paths) {
if (path.parts == null) {
// No parts means that all path parameters are optional!
urlPartsRequired = new Set()
break
}
urlPartsRequired = new Set([...Object.keys(path.parts)].filter((x) => urlPartsRequired.has(x)))
}
// transform [{name: ..., required: ...}] -> {name: {required: ...}}
const pathPropertyMap: Record<string, model.Property> = requestProperties.path.reduce((prev, prop) => ({ ...prev, [prop.name]: prop }), {})
for (const name of pathProperties) {
// okay to skip if it's not included since this scenario
// is covered above with a different error.
if (!urlParts.includes(name)) {
continue
}
// Find the mismatches between the specs
if (urlPartsRequired.has(name) && !pathPropertyMap[name].required) {
errors.addEndpointError(endpoint.name, 'request', `${endpoint.request.name}: path parameter '${name}' is required in the json spec`)
} else if (!urlPartsRequired.has(name) && pathPropertyMap[name].required) {
errors.addEndpointError(endpoint.name, 'request', `${endpoint.request.name}: path parameter '${name}' is optional in the json spec`)
}
}
// fleet API are deliberately undocumented in rest-api-spec)
if (spec.params != null && !endpoint.name.startsWith('fleet.')) {
const params = Object.keys(spec.params)
const queryProperties = requestProperties.query.map(property => property.name)
// are all the parameters in the request definition present in the json spec?
for (const name of queryProperties) {
if (!params.includes(name)) {
errors.addEndpointError(endpoint.name, 'request', `${endpoint.request.name}: query parameter '${name}' does not exist in the json spec`)
}
}
// are all the parameters in the json spec present in the request definition?
for (const name of params) {
if (!queryProperties.includes(name)) {
errors.addEndpointError(endpoint.name, 'request', `${endpoint.request.name}: missing json spec query parameter '${name}'`)
}
}
}
if (requestProperties.body === Body.yesBody && spec.body == null) {
errors.addEndpointError(endpoint.name, 'request', `${endpoint.request.name}: should not have a body`)
}
if (requestProperties.body === Body.noBody && spec.body != null && spec.body.required === true) {
errors.addEndpointError(endpoint.name, 'request', `${endpoint.request.name}: should have a body definition`)
}
if (spec.body != null && spec.body.required === true && spec.body.required !== endpoint.requestBodyRequired) {
errors.addEndpointError(endpoint.name, 'request', ': should not be an optional body definition')
}
}
}
return model
function getDefinition (name: model.TypeName): model.Request | model.Interface {
for (const type of model.types) {
if (type.kind === 'request' || type.kind === 'interface') {
if (type.name.name === name.name && type.name.namespace === name.namespace) {
return type
}
}
}
throw new Error(`Can't find the request definiton for ${name.namespace}.${name.name}`)
}
// recursively gets the properties from the current and inherited classes
function getProperties (definition: model.Request | model.Interface): { path: model.Property[], query: model.Property[], body: Body } {
const path: model.Property[] = []
const query: model.Property[] = []
let body: Body = Body.noBody
if (definition.kind === 'request') {
if (definition.path.length > 0) {
path.push(...definition.path)
}
if (definition.query.length > 0) {
query.push(...definition.query)
}
if (definition.body.kind !== 'no_body') {
body = Body.yesBody
}
if (definition.attachedBehaviors != null) {
for (const attachedBehavior of definition.attachedBehaviors) {
const type_ = getDefinition({
namespace: '_spec_utils',
name: attachedBehavior
})
if (
type_.kind === 'interface' &&
// allowing CommonQueryParameters too generates many errors
attachedBehavior === 'CommonCatQueryParameters'
) {
for (const prop of type_.properties) {
query.push(prop)
}
}
}
}
} else {
if (definition.properties.length > 0) {
query.push(...definition.properties)
}
}
return { path, query, body }
}
}