forked from elastic/elasticsearch-specification
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathvalidate-rest-spec.ts
189 lines (165 loc) · 7.46 KB
/
validate-rest-spec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import assert from 'assert'
import * as model from '../model/metamodel'
import { JsonSpec } from '../model/json-spec'
import { ValidationErrors } from '../validation-errors'
// This code can be simplified once https://github.com/tc39/proposal-set-methods is available
enum Body {
noBody = 0,
yesBody = 1
}
const LOG = 'ALL' // default: ALL
/**
* Validates the model against the rest-api-spec.
* It verifies is the model has exactly the same path and query parameters,
* furthermore it verifies if the body is required or not.
* If a validation fails, it will log a warning.
*/
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}`)
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`)
}
}
if (spec.params != null) {
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
}
} else {
if (definition.properties.length > 0) {
query.push(...definition.properties)
}
}
if (Array.isArray(definition.inherits)) {
const inherits = definition.inherits.map(inherit => getDefinition(inherit.type))
for (const inherit of inherits) {
const properties = getProperties(inherit)
if (properties.path.length > 0) {
path.push(...properties.path)
}
if (properties.query.length > 0) {
query.push(...properties.query)
}
if (properties.body === Body.yesBody) {
body = properties.body
}
}
}
return { path, query, body }
}
}