Skip to content

[Backport 8.1] Add failures to spec #1573

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 55 additions & 4 deletions compiler/src/model/build-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import { writeFileSync } from 'fs'
import { join } from 'path'
import { STATUS_CODES } from 'http'
import {
ClassDeclaration,
EnumDeclaration,
Expand Down Expand Up @@ -320,8 +321,8 @@ function compileClassOrInterfaceDeclaration (declaration: ClassDeclaration | Int
Node.isPropertyDeclaration(member) || Node.isPropertySignature(member),
'Class and interfaces can only have property declarations or signatures'
)
const property = visitRequestOrResponseProperty(member)
if (property.name === 'body') {
if (member.getName() === 'body') {
const property = visitRequestOrResponseProperty(member)
// the body can either by a value (eg Array<string> or an object with properties)
if (property.valueOf != null) {
if (property.valueOf.kind === 'instance_of' && property.valueOf.type.name === 'Void') {
Expand All @@ -332,8 +333,58 @@ function compileClassOrInterfaceDeclaration (declaration: ClassDeclaration | Int
} else {
type.body = { kind: 'properties', properties: property.properties }
}
} else if (member.getName() === 'exceptions') {
const exceptions: model.ResponseException[] = []
const property = member.getTypeNode()
assert(
property,
Node.isTupleTypeNode(property),
'Failures should be an array.'
)
for (const element of property.getElements()) {
const exception: model.ResponseException = {
statusCodes: [],
body: { kind: 'no_body' }
}
element.forEachChild(child => {
assert(
child,
Node.isPropertySignature(child) || Node.isPropertyDeclaration(child),
`Children should be ${ts.SyntaxKind[ts.SyntaxKind.PropertySignature]} or ${ts.SyntaxKind[ts.SyntaxKind.PropertyDeclaration]} but is ${ts.SyntaxKind[child.getKind()]} instead`
)
const jsDocs = child.getJsDocs()
if (jsDocs.length > 0) {
exception.description = jsDocs[0].getDescription()
}
if (child.getName() === 'statusCodes') {
const value = child.getTypeNode()
assert(value, Node.isTupleTypeNode(value), 'statusCodes should be an array.')
for (const code of value.getElements()) {
assert(code, Node.isLiteralTypeNode(code) && Number.isInteger(Number(code.getText())), 'Status code values should a valid integer')
assert(code, STATUS_CODES[code.getText()] != null, `${code.getText()} is not a valid status code`)
exception.statusCodes.push(Number(code.getText()))
}
} else if (child.getName() === 'body') {
const property = visitRequestOrResponseProperty(child)
// the body can either by a value (eg Array<string> or an object with properties)
if (property.valueOf != null) {
if (property.valueOf.kind === 'instance_of' && property.valueOf.type.name === 'Void') {
exception.body = { kind: 'no_body' }
} else {
exception.body = { kind: 'value', value: property.valueOf }
}
} else {
exception.body = { kind: 'properties', properties: property.properties }
}
} else {
assert(child, false, 'Failure.body and Failure.statusCode are the only Failure properties supported')
}
})
exceptions.push(exception)
}
type.exceptions = exceptions
} else {
assert(member, false, 'Response.body is the only Response property supported')
assert(member, false, 'Response.body and Response.failures are the only Response properties supported')
}
}
}
Expand Down Expand Up @@ -500,7 +551,7 @@ function visitRequestOrResponseProperty (member: PropertyDeclaration | PropertyS
// declaration is a child of the top level properties, while TypeReference should
// directly by "unwrapped" with `modelType` because if you navigate the children of a TypeReference
// you will lose the context and crafting the types becomes really hard.
if (Node.isTypeReference(value)) {
if (Node.isTypeReference(value) || Node.isUnionTypeNode(value)) {
valueOf = modelType(value)
} else {
value.forEachChild(child => {
Expand Down
7 changes: 7 additions & 0 deletions compiler/src/model/metamodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,13 @@ export class Response extends BaseType {
body: Body
behaviors?: Inherits[]
attachedBehaviors?: string[]
exceptions?: ResponseException[]
}

export class ResponseException {
description?: string
body: Body
statusCodes: number[]
}

export type Body = ValueBody | PropertiesBody | NoBody
Expand Down
39 changes: 32 additions & 7 deletions docs/add-new-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,13 @@ Following you can find a template valid for any request definition.
interface Request extends RequestBase {
path_parts: {

};
}
query_parameters: {

};
}
body: {

};
}
}
```

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

};
}
query_parameters: {

};
}
body: {

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

#### Generics

In some cases, the response could take one or more generics, in such case the definition will be:

```ts
class Response<Generic> {
body: {

key: Generic
}
}
```

And the generic will be used somewhere inside the definition.

#### Exceptions

Normally, every API returns the exact same structure in case of error, which is defined
in [`ErrorResponseBase`](https://github.com/elastic/elasticsearch-specification/blob/main/specification/_types/Base.ts#L66-L75).
In some edge cases, the response structure may change. To document this situations, you should use the `exceptions` key.

```ts
class Response {
body: SuccessResponseBodyType
exceptions: [
{
/**
* A brief description of the exception.
*/
statusCodes: [404]
body: NotFoundException
}
]
}
```
35 changes: 1 addition & 34 deletions docs/specification-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,37 +45,4 @@ For example: [`/specification/_global/search`](../specification/_global/search).
### Specification helpers

The specification has a set of custom types used to define complex structures
or behaviors. Those types must be placed in [`/specification/_spec_utils`](../specification/_spec_utils).

### Import alias

It can happen that you need to import a type from a different namespace that already exist in the current file.
To solve this problem, you should use import aliases.

```ts
// _global/get/GetRequest.ts

import { RequestBase } from '@_types/Base'

/**
* @rest_spec_name get
* @since 0.0.0
* @stability stable | beta | experimental
*/
export interface Request extends RequestBase {
path_parts: {}
query_parameters: {}
}
```
```ts
// _global/get_source/SourceRequest.ts

import { Request as GetRequest } from '_global/get/GetRequest'

/**
* @rest_spec_name get_source
* @since 0.0.0
* @stability stable | beta | experimental
*/
export interface Request extends GetRequest {}
```
or behaviors. Those types must be placed in [`/specification/_spec_utils`](../specification/_spec_utils).
Loading