Skip to content

Commit 8fd84d3

Browse files
authored
Changes "void_value" into "no_body" for req/responses with no body (#444)
1 parent f6e549b commit 8fd84d3

File tree

10 files changed

+2419
-329
lines changed

10 files changed

+2419
-329
lines changed

compiler/model/build-model.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ function compileClassOrInterfaceDeclaration (declaration: ClassDeclaration | Int
179179
)
180180
}
181181

182+
let bodyWasSet = false
183+
182184
// Request and Response definitions needs to be handled
183185
// differently from normal classes
184186
if (name === 'Request' || name === 'Response') {
@@ -188,7 +190,8 @@ function compileClassOrInterfaceDeclaration (declaration: ClassDeclaration | Int
188190
kind: 'request',
189191
name: { name, namespace: getNameSpace(declaration) },
190192
path: new Array<model.Property>(),
191-
query: new Array<model.Property>()
193+
query: new Array<model.Property>(),
194+
body: { kind: 'no_body' }
192195
}
193196

194197
const response: model.TypeName = {
@@ -210,20 +213,27 @@ function compileClassOrInterfaceDeclaration (declaration: ClassDeclaration | Int
210213
type.path = property.properties
211214
} else if (property.name === 'query_parameters') {
212215
type.query = property.properties
213-
} else {
214-
// the body can either by a value (eg Array<string> or an object with properties)
216+
} else if (property.name === 'body') {
217+
bodyWasSet = true
218+
// the body can either be a value (eg Array<string> or an object with properties)
215219
if (property.valueOf != null) {
216-
type.body = { kind: 'value', value: property.valueOf }
220+
if (property.valueOf.kind === 'instance_of' && property.valueOf.type.name === 'Void') {
221+
type.body = { kind: 'no_body' }
222+
} else {
223+
type.body = { kind: 'value', value: property.valueOf }
224+
}
217225
} else if (property.properties.length > 0) {
218226
type.body = { kind: 'properties', properties: property.properties }
219227
}
228+
} else {
229+
assert(member, false, 'Unknown request property ' + property.name)
220230
}
221231
}
222232
} else {
223233
type = {
224234
kind: 'response',
225235
name: { name, namespace: getNameSpace(declaration) },
226-
body: undefined
236+
body: { kind: 'no_body' }
227237
}
228238

229239
for (const member of declaration.getMembers()) {
@@ -235,9 +245,14 @@ function compileClassOrInterfaceDeclaration (declaration: ClassDeclaration | Int
235245
)
236246
const property = visitRequestOrResponseProperty(member)
237247
if (property.name === 'body') {
248+
bodyWasSet = true
238249
// the body can either by a value (eg Array<string> or an object with properties)
239250
if (property.valueOf != null) {
240-
type.body = { kind: 'value', value: property.valueOf }
251+
if (property.valueOf.kind === 'instance_of' && property.valueOf.type.name === 'Void') {
252+
type.body = { kind: 'no_body' }
253+
} else {
254+
type.body = { kind: 'value', value: property.valueOf }
255+
}
241256
} else if (property.properties.length > 0) {
242257
type.body = { kind: 'properties', properties: property.properties }
243258
}
@@ -279,6 +294,15 @@ function compileClassOrInterfaceDeclaration (declaration: ClassDeclaration | Int
279294
})
280295
}
281296

297+
// If the body wasn't set and we have a parent class, then it's a property body with no additional properties
298+
if (!bodyWasSet && type.inherits != null) {
299+
const parent = type.inherits.type
300+
// RequestBase is special as it's a "marker" base class that doesn't imply a property body type. We should get rid of it.
301+
if (!(parent.name === 'RequestBase' && parent.namespace === '_types')) {
302+
type.body = { kind: 'properties', properties: new Array<model.Property>() }
303+
}
304+
}
305+
282306
return type
283307

284308
// Every other class or interface will be handled here

compiler/model/metamodel.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export class TypeName {
4949
/**
5050
* Type of a value. Used both for property types and nested type definitions.
5151
*/
52-
export type ValueOf = InstanceOf | ArrayOf | UnionOf | DictionaryOf | UserDefinedValue | LiteralValue | VoidValue
52+
export type ValueOf = InstanceOf | ArrayOf | UnionOf | DictionaryOf | UserDefinedValue | LiteralValue
5353

5454
/**
5555
* A single value
@@ -116,13 +116,6 @@ export class LiteralValue {
116116
value: string | number | boolean
117117
}
118118

119-
/**
120-
* The absence of any type. This is commonly used in APIs that returns an empty body.
121-
*/
122-
export class VoidValue {
123-
kind: 'void_value'
124-
}
125-
126119
/**
127120
* An interface or request interface property.
128121
*/
@@ -225,6 +218,7 @@ export class Request extends BaseType {
225218
// Note: does not extend Interface as properties are split across path, query and body
226219
kind: 'request'
227220
generics?: TypeName[]
221+
/** The parent defines additional body properties that are added to the body, that has to be a PropertyBody */
228222
inherits?: Inherits
229223
implements?: Inherits[]
230224
/** URL path properties */
@@ -239,10 +233,11 @@ export class Request extends BaseType {
239233
// We can also pull path parameter descriptions on body properties they replace
240234

241235
/**
242-
* Body type. In most cases this is just a list of properties, except for a few specific cases like bulk requests
243-
* (an array of bulk operations) or create requests (a user provided document type).
236+
* Body type. Most often a list of properties (that can extend those of the inherited class, see above), except for a
237+
* few specific cases that use other types such as bulk (array) or create (generic parameter). Or NoBody for requests
238+
* that don't have a body.
244239
*/
245-
body?: ValueBody | PropertiesBody
240+
body: Body
246241
behaviors?: Inherits[]
247242
attachedBehaviors?: string[]
248243
}
@@ -255,11 +250,13 @@ export class Response extends BaseType {
255250
generics?: TypeName[]
256251
inherits?: Inherits
257252
implements?: Inherits[]
258-
body?: ValueBody | PropertiesBody
253+
body: Body
259254
behaviors?: Inherits[]
260255
attachedBehaviors?: string[]
261256
}
262257

258+
export type Body = ValueBody | PropertiesBody | NoBody
259+
263260
export class ValueBody {
264261
kind: 'value'
265262
value: ValueOf
@@ -270,6 +267,10 @@ export class PropertiesBody {
270267
properties: Property[]
271268
}
272269

270+
export class NoBody {
271+
kind: 'no_body'
272+
}
273+
273274
/**
274275
* An enumeration member.
275276
*

compiler/model/utils.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -247,13 +247,6 @@ export function modelType (node: Node): model.ValueOf {
247247
return type
248248
}
249249

250-
case 'Void': {
251-
const type: model.VoidValue = {
252-
kind: 'void_value'
253-
}
254-
return type
255-
}
256-
257250
default: {
258251
const generics = node.getTypeArguments().map(node => modelType(node))
259252
const identifier = node.getTypeName()

compiler/steps/validate-model.ts

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -392,22 +392,28 @@ export default async function validateModel (apiModel: model.Model, restSpec: Ma
392392
validateProperties(typeDef.query, openGenerics)
393393
context.pop()
394394

395-
if (typeDef.body != null) {
396-
context.push('body')
397-
switch (typeDef.body.kind) {
398-
case 'properties':
399-
validateProperties(typeDef.body.properties, openGenerics)
400-
break
401-
case 'value':
402-
validateValueOf(typeDef.body.value, openGenerics)
403-
break
404-
default:
405-
// @ts-expect-error
406-
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
407-
throw new Error(`Unknown kind: ${typeDef.body.kind}`)
395+
context.push('body')
396+
if (typeDef.inherits != null && typeDef.body.kind !== 'properties') {
397+
if (fqn(typeDef.inherits.type) !== '_types:RequestBase') {
398+
modelError('A request with inherited properties must have a PropertyBody')
408399
}
409-
context.pop()
410400
}
401+
switch (typeDef.body.kind) {
402+
case 'properties':
403+
validateProperties(typeDef.body.properties, openGenerics)
404+
break
405+
case 'value':
406+
validateValueOf(typeDef.body.value, openGenerics)
407+
break
408+
case 'no_body':
409+
// Nothing to validate
410+
break
411+
default:
412+
// @ts-expect-error
413+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
414+
throw new Error(`Unknown kind: ${typeDef.body.kind}`)
415+
}
416+
context.pop()
411417
}
412418

413419
function validateResponse (typeDef: model.Response): void {
@@ -421,22 +427,28 @@ export default async function validateModel (apiModel: model.Model, restSpec: Ma
421427
// valid overlaps, with some parameters that can also be represented as body properties.
422428
// Client generators will have to take care of this.
423429

424-
if (typeDef.body != null) {
425-
context.push('body')
426-
switch (typeDef.body.kind) {
427-
case 'properties':
428-
validateProperties(typeDef.body.properties, openGenerics)
429-
break
430-
case 'value':
431-
validateValueOf(typeDef.body.value, openGenerics)
432-
break
433-
default:
434-
// @ts-expect-error
435-
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
436-
throw new Error(`Unknown kind: ${typeDef.body.kind}`)
430+
context.push('body')
431+
if (typeDef.inherits != null && typeDef.body.kind !== 'properties') {
432+
if (fqn(typeDef.inherits.type) !== '_types:RequestBase') {
433+
modelError('A response with inherited properties must have a PropertyBody')
437434
}
438-
context.pop()
439435
}
436+
switch (typeDef.body.kind) {
437+
case 'properties':
438+
validateProperties(typeDef.body.properties, openGenerics)
439+
break
440+
case 'value':
441+
validateValueOf(typeDef.body.value, openGenerics)
442+
break
443+
case 'no_body':
444+
// Nothing to validate
445+
break
446+
default:
447+
// @ts-expect-error
448+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
449+
throw new Error(`Unknown kind: ${typeDef.body.kind}`)
450+
}
451+
context.pop()
440452
}
441453

442454
function validateInterface (typeDef: model.Interface): void {
@@ -727,10 +739,6 @@ export default async function validateModel (apiModel: model.Model, restSpec: Ma
727739
// Nothing to validate
728740
break
729741

730-
case 'void_value':
731-
// Nothing to validate
732-
break
733-
734742
default:
735743
// @ts-expect-error
736744
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions

compiler/validation-errors.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,10 @@ export class ValidationErrors {
6868
let count = 0
6969
const logArray = function (errs: string[], prefix = ''): void {
7070
for (const err of errs) {
71-
console.error(`${prefix}${err}`)
72-
count++
71+
if (err !== 'Endpoint has "@stability: TODO' && err !== 'Missing request & response') {
72+
console.error(`${prefix}${err}`)
73+
count++
74+
}
7375
}
7476
}
7577

0 commit comments

Comments
 (0)