Skip to content

Commit cf30c6b

Browse files
[Backport 8.1] Add failures to spec (#1573)
Co-authored-by: Tomas Della Vedova <[email protected]>
1 parent ca3df22 commit cf30c6b

File tree

10 files changed

+285
-65
lines changed

10 files changed

+285
-65
lines changed

compiler/src/model/build-model.ts

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import { writeFileSync } from 'fs'
2121
import { join } from 'path'
22+
import { STATUS_CODES } from 'http'
2223
import {
2324
ClassDeclaration,
2425
EnumDeclaration,
@@ -320,8 +321,8 @@ function compileClassOrInterfaceDeclaration (declaration: ClassDeclaration | Int
320321
Node.isPropertyDeclaration(member) || Node.isPropertySignature(member),
321322
'Class and interfaces can only have property declarations or signatures'
322323
)
323-
const property = visitRequestOrResponseProperty(member)
324-
if (property.name === 'body') {
324+
if (member.getName() === 'body') {
325+
const property = visitRequestOrResponseProperty(member)
325326
// the body can either by a value (eg Array<string> or an object with properties)
326327
if (property.valueOf != null) {
327328
if (property.valueOf.kind === 'instance_of' && property.valueOf.type.name === 'Void') {
@@ -332,8 +333,58 @@ function compileClassOrInterfaceDeclaration (declaration: ClassDeclaration | Int
332333
} else {
333334
type.body = { kind: 'properties', properties: property.properties }
334335
}
336+
} else if (member.getName() === 'exceptions') {
337+
const exceptions: model.ResponseException[] = []
338+
const property = member.getTypeNode()
339+
assert(
340+
property,
341+
Node.isTupleTypeNode(property),
342+
'Failures should be an array.'
343+
)
344+
for (const element of property.getElements()) {
345+
const exception: model.ResponseException = {
346+
statusCodes: [],
347+
body: { kind: 'no_body' }
348+
}
349+
element.forEachChild(child => {
350+
assert(
351+
child,
352+
Node.isPropertySignature(child) || Node.isPropertyDeclaration(child),
353+
`Children should be ${ts.SyntaxKind[ts.SyntaxKind.PropertySignature]} or ${ts.SyntaxKind[ts.SyntaxKind.PropertyDeclaration]} but is ${ts.SyntaxKind[child.getKind()]} instead`
354+
)
355+
const jsDocs = child.getJsDocs()
356+
if (jsDocs.length > 0) {
357+
exception.description = jsDocs[0].getDescription()
358+
}
359+
if (child.getName() === 'statusCodes') {
360+
const value = child.getTypeNode()
361+
assert(value, Node.isTupleTypeNode(value), 'statusCodes should be an array.')
362+
for (const code of value.getElements()) {
363+
assert(code, Node.isLiteralTypeNode(code) && Number.isInteger(Number(code.getText())), 'Status code values should a valid integer')
364+
assert(code, STATUS_CODES[code.getText()] != null, `${code.getText()} is not a valid status code`)
365+
exception.statusCodes.push(Number(code.getText()))
366+
}
367+
} else if (child.getName() === 'body') {
368+
const property = visitRequestOrResponseProperty(child)
369+
// the body can either by a value (eg Array<string> or an object with properties)
370+
if (property.valueOf != null) {
371+
if (property.valueOf.kind === 'instance_of' && property.valueOf.type.name === 'Void') {
372+
exception.body = { kind: 'no_body' }
373+
} else {
374+
exception.body = { kind: 'value', value: property.valueOf }
375+
}
376+
} else {
377+
exception.body = { kind: 'properties', properties: property.properties }
378+
}
379+
} else {
380+
assert(child, false, 'Failure.body and Failure.statusCode are the only Failure properties supported')
381+
}
382+
})
383+
exceptions.push(exception)
384+
}
385+
type.exceptions = exceptions
335386
} else {
336-
assert(member, false, 'Response.body is the only Response property supported')
387+
assert(member, false, 'Response.body and Response.failures are the only Response properties supported')
337388
}
338389
}
339390
}
@@ -500,7 +551,7 @@ function visitRequestOrResponseProperty (member: PropertyDeclaration | PropertyS
500551
// declaration is a child of the top level properties, while TypeReference should
501552
// directly by "unwrapped" with `modelType` because if you navigate the children of a TypeReference
502553
// you will lose the context and crafting the types becomes really hard.
503-
if (Node.isTypeReference(value)) {
554+
if (Node.isTypeReference(value) || Node.isUnionTypeNode(value)) {
504555
valueOf = modelType(value)
505556
} else {
506557
value.forEachChild(child => {

compiler/src/model/metamodel.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,13 @@ export class Response extends BaseType {
273273
body: Body
274274
behaviors?: Inherits[]
275275
attachedBehaviors?: string[]
276+
exceptions?: ResponseException[]
277+
}
278+
279+
export class ResponseException {
280+
description?: string
281+
body: Body
282+
statusCodes: number[]
276283
}
277284

278285
export type Body = ValueBody | PropertiesBody | NoBody

docs/add-new-api.md

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,13 @@ Following you can find a template valid for any request definition.
6767
interface Request extends RequestBase {
6868
path_parts: {
6969

70-
};
70+
}
7171
query_parameters: {
7272

73-
};
73+
}
7474
body: {
7575

76-
};
76+
}
7777
}
7878
```
7979

@@ -87,13 +87,13 @@ In some cases, the request could take one or more generics, in such case the def
8787
interface Request<Generic> extends RequestBase {
8888
path_parts: {
8989

90-
};
90+
}
9191
query_parameters: {
9292

93-
};
93+
}
9494
body: {
9595

96-
};
96+
}
9797
}
9898
```
9999
And the generic will be used somewhere inside the definition.
@@ -118,12 +118,37 @@ class Response {
118118
As you can see, for responses there are no custom top level keys, as the
119119
response definition represents the body of a succesful response.
120120

121+
#### Generics
122+
121123
In some cases, the response could take one or more generics, in such case the definition will be:
124+
122125
```ts
123126
class Response<Generic> {
124127
body: {
125-
128+
key: Generic
126129
}
127130
}
128131
```
132+
129133
And the generic will be used somewhere inside the definition.
134+
135+
#### Exceptions
136+
137+
Normally, every API returns the exact same structure in case of error, which is defined
138+
in [`ErrorResponseBase`](https://github.com/elastic/elasticsearch-specification/blob/main/specification/_types/Base.ts#L66-L75).
139+
In some edge cases, the response structure may change. To document this situations, you should use the `exceptions` key.
140+
141+
```ts
142+
class Response {
143+
body: SuccessResponseBodyType
144+
exceptions: [
145+
{
146+
/**
147+
* A brief description of the exception.
148+
*/
149+
statusCodes: [404]
150+
body: NotFoundException
151+
}
152+
]
153+
}
154+
```

docs/specification-structure.md

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -45,37 +45,4 @@ For example: [`/specification/_global/search`](../specification/_global/search).
4545
### Specification helpers
4646

4747
The specification has a set of custom types used to define complex structures
48-
or behaviors. Those types must be placed in [`/specification/_spec_utils`](../specification/_spec_utils).
49-
50-
### Import alias
51-
52-
It can happen that you need to import a type from a different namespace that already exist in the current file.
53-
To solve this problem, you should use import aliases.
54-
55-
```ts
56-
// _global/get/GetRequest.ts
57-
58-
import { RequestBase } from '@_types/Base'
59-
60-
/**
61-
* @rest_spec_name get
62-
* @since 0.0.0
63-
* @stability stable | beta | experimental
64-
*/
65-
export interface Request extends RequestBase {
66-
path_parts: {}
67-
query_parameters: {}
68-
}
69-
```
70-
```ts
71-
// _global/get_source/SourceRequest.ts
72-
73-
import { Request as GetRequest } from '_global/get/GetRequest'
74-
75-
/**
76-
* @rest_spec_name get_source
77-
* @since 0.0.0
78-
* @stability stable | beta | experimental
79-
*/
80-
export interface Request extends GetRequest {}
81-
```
48+
or behaviors. Those types must be placed in [`/specification/_spec_utils`](../specification/_spec_utils).

0 commit comments

Comments
 (0)