Skip to content

Commit 0ecaecd

Browse files
virkt25raymondfeng
andcommitted
feat(openapi-v3): add support for openapi responses
Co-Authored-By: Raymond Feng <raymondfeng@users.noreply.github.com>
1 parent a69f20e commit 0ecaecd

File tree

11 files changed

+235
-60
lines changed

11 files changed

+235
-60
lines changed

packages/openapi-spec-builder/src/openapi-spec-builder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export class OpenApiSpecBuilder extends BuilderBase<OpenApiSpec> {
120120
export class OperationSpecBuilder extends BuilderBase<OperationObject> {
121121
constructor() {
122122
super({
123-
responses: {},
123+
responses: {'200': {description: 'An undocumented response body.'}},
124124
});
125125
}
126126

packages/openapi-v3/src/controller-spec.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ import {getJsonSchema} from '@loopback/repository-json-schema';
1616
import {OAI3Keys} from './keys';
1717
import {jsonToSchemaObject} from './json-to-schema';
1818
import * as _ from 'lodash';
19+
import {resolveSchema} from './generate-schema';
1920

20-
const debug = require('debug')('loopback:openapi3:metadata');
21+
const debug = require('debug')('loopback:openapi3:metadata:controller-spec');
2122

2223
// tslint:disable:no-any
2324

@@ -89,16 +90,51 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {
8990
endpointName = `${fullMethodName} (${verb} ${path})`;
9091
}
9192

93+
const defaultResponse = {
94+
'200': {
95+
description: `Return value of ${constructor.name}.${op}`,
96+
},
97+
};
98+
9299
let operationSpec = endpoint.spec;
93100
if (!operationSpec) {
94101
// The operation was defined via @operation(verb, path) with no spec
95102
operationSpec = {
96-
responses: {},
103+
responses: defaultResponse,
97104
};
98105
endpoint.spec = operationSpec;
99106
}
100107
debug(' operation for method %s: %j', op, endpoint);
101108

109+
debug(' spec responses for method %s: %o', op, operationSpec.responses);
110+
111+
const TS_TYPE_KEY = 'x-ts-type';
112+
113+
for (const code in operationSpec.responses) {
114+
for (const c in operationSpec.responses[code].content) {
115+
debug(' evaluating response code %s with content: %o', code, c);
116+
const content = operationSpec.responses[code].content[c];
117+
const tsType = content[TS_TYPE_KEY];
118+
debug(' %s => %o', TS_TYPE_KEY, tsType);
119+
if (tsType) {
120+
content.schema = resolveSchema(tsType, content.schema);
121+
122+
// We don't want a Function type in the final spec.
123+
delete content[TS_TYPE_KEY];
124+
}
125+
126+
if (content.schema.type === 'array') {
127+
content.schema.items = resolveSchema(
128+
content.schema.items[TS_TYPE_KEY],
129+
content.schema.items,
130+
);
131+
132+
// We don't want a Function type in the final spec.
133+
delete content.schema.items[TS_TYPE_KEY];
134+
}
135+
}
136+
}
137+
102138
debug(' processing parameters for method %s', op);
103139
let params = MetadataInspector.getAllParameterMetadata<ParameterObject>(
104140
OAI3Keys.PARAMETERS_KEY,

packages/openapi-v3/src/decorators/parameter.decorator.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
SchemaObject,
1212
} from '@loopback/openapi-v3-types';
1313
import {MetadataInspector, ParameterDecoratorFactory} from '@loopback/context';
14-
import {getSchemaForParam} from '../generate-schema';
14+
import {resolveSchema} from '../generate-schema';
1515
import {OAI3Keys} from '../keys';
1616

1717
/**
@@ -48,8 +48,8 @@ export function param(paramSpec: ParameterObject) {
4848
// generate schema if `paramSpec` has `schema` but without `type`
4949
(isSchemaObject(paramSpec.schema) && !paramSpec.schema.type)
5050
) {
51-
// please note `getSchemaForParam` only adds `type` and `format` for `schema`
52-
paramSpec.schema = getSchemaForParam(paramType, paramSpec.schema);
51+
// please note `resolveSchema` only adds `type` and `format` for `schema`
52+
paramSpec.schema = resolveSchema(paramType, paramSpec.schema);
5353
}
5454
}
5555

packages/openapi-v3/src/decorators/request-body.decorator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
ReferenceObject,
1010
} from '@loopback/openapi-v3-types';
1111
import {MetadataInspector, ParameterDecoratorFactory} from '@loopback/context';
12-
import {getSchemaForRequestBody} from '../generate-schema';
12+
import {resolveSchema} from '../generate-schema';
1313
import {OAI3Keys} from '../keys';
1414
import * as _ from 'lodash';
1515
import {inspect} from 'util';
@@ -94,7 +94,7 @@ export function requestBody(requestBodySpec?: Partial<RequestBodyObject>) {
9494
const paramTypes = (methodSig && methodSig.parameterTypes) || [];
9595

9696
const paramType = paramTypes[index];
97-
const schema = getSchemaForRequestBody(paramType);
97+
const schema = resolveSchema(paramType);
9898
debug(' inferred schema: %s', inspect(schema, {depth: null}));
9999
requestBodySpec.content = _.mapValues(requestBodySpec.content, c => {
100100
if (!c.schema) {

packages/openapi-v3/src/generate-schema.ts

Lines changed: 21 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,6 @@
55

66
import {SchemaObject} from '@loopback/openapi-v3-types';
77

8-
/**
9-
* @private
10-
*/
11-
interface TypeAndFormat {
12-
type?: string;
13-
format?: string;
14-
}
15-
168
/**
179
* Generate the `type` and `format` property in a Schema Object according to a
1810
* parameter's type.
@@ -22,44 +14,29 @@ interface TypeAndFormat {
2214
* @param type The JavaScript type of a parameter
2315
* @param schema The schema object provided in an parameter object
2416
*/
25-
export function getSchemaForParam(
26-
type: Function,
27-
schema?: SchemaObject,
17+
export function resolveSchema(
18+
fn?: Function,
19+
schema: SchemaObject = {},
2820
): SchemaObject {
29-
schema = schema || {};
30-
// preserve `type` and `format` provided by user
31-
if (schema.type && schema.format) return schema;
21+
let resolvedSchema: SchemaObject = {};
3222

33-
let typeAndFormat: TypeAndFormat = {};
34-
if (type === String) {
35-
typeAndFormat.type = 'string';
36-
} else if (type === Number) {
37-
typeAndFormat.type = 'number';
38-
} else if (type === Boolean) {
39-
typeAndFormat.type = 'boolean';
40-
} else if (type === Array) {
41-
// item type cannot be inspected
42-
typeAndFormat.type = 'array';
43-
} else if (type === Object) {
44-
typeAndFormat.type = 'object';
23+
if (typeof fn === 'function') {
24+
if (fn === String) {
25+
resolvedSchema = {type: 'string'};
26+
} else if (fn === Number) {
27+
resolvedSchema = {type: 'number'};
28+
} else if (fn === Boolean) {
29+
resolvedSchema = {type: 'boolean'};
30+
} else if (fn === Date) {
31+
resolvedSchema = {type: 'string', format: 'date'};
32+
} else if (fn === Object) {
33+
resolvedSchema = {type: 'object'};
34+
} else if (fn === Array) {
35+
resolvedSchema = {type: 'array'};
36+
} else {
37+
resolvedSchema = {$ref: `#/components/schemas/${fn.name}`};
38+
}
4539
}
4640

47-
if (typeAndFormat.type && !schema.type) schema.type = typeAndFormat.type;
48-
if (typeAndFormat.format && !schema.format)
49-
schema.format = typeAndFormat.format;
50-
51-
return schema;
52-
}
53-
54-
/**
55-
* Get OpenAPI Schema for a JavaScript type for a body parameter
56-
*
57-
* @private
58-
* @param type The JavaScript type of an argument deccorated by @requestBody
59-
*/
60-
export function getSchemaForRequestBody(type: Function): SchemaObject {
61-
let generatedSchema = getSchemaForParam(type);
62-
if (!generatedSchema.type)
63-
generatedSchema.$ref = '#/components/schemas/' + type.name;
64-
return generatedSchema;
41+
return Object.assign(schema, resolvedSchema);
6542
}

packages/openapi-v3/test/integration/controller-spec.integration.ts

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import {expect} from '@loopback/testlab';
77
import {model, property} from '@loopback/repository';
88
import {ParameterObject} from '@loopback/openapi-v3-types';
9-
import {param, requestBody, getControllerSpec, post} from '../../';
9+
import {param, requestBody, getControllerSpec, post, get} from '../../';
1010

1111
describe('controller spec', () => {
1212
it('adds property schemas in components.schemas', () => {
@@ -41,7 +41,11 @@ describe('controller spec', () => {
4141
paths: {
4242
'/foo': {
4343
post: {
44-
responses: {},
44+
responses: {
45+
'200': {
46+
description: 'Return value of FooController.create',
47+
},
48+
},
4549
requestBody: {
4650
description: 'a foo instance',
4751
required: true,
@@ -105,6 +109,7 @@ describe('controller spec', () => {
105109
expect(schemas).to.have.keys('MyParam', 'Foo');
106110
expect(schemas!.MyParam).to.not.have.key('definitions');
107111
});
112+
108113
it('infers no properties if no property metadata is present', () => {
109114
const paramSpec: ParameterObject = {
110115
name: 'foo',
@@ -148,4 +153,102 @@ describe('controller spec', () => {
148153
expect(schemas).to.have.key('MyParam');
149154
expect(schemas!.MyParam).to.deepEqual({});
150155
});
156+
157+
it('generates a default responses object if not set', () => {
158+
class MyController {
159+
@get('/')
160+
hello() {
161+
return 'hello world';
162+
}
163+
}
164+
165+
const spec = getControllerSpec(MyController);
166+
expect(spec.paths['/'].get).to.have.property('responses');
167+
expect(spec.paths['/'].get.responses).to.eql({
168+
'200': {
169+
description: 'Return value of MyController.hello',
170+
},
171+
});
172+
});
173+
174+
it('generates a response given no content property', () => {
175+
class MyController {
176+
@get('/', {
177+
responses: {
178+
'200': {
179+
description: 'hello world',
180+
},
181+
},
182+
})
183+
hello() {
184+
return 'hello world';
185+
}
186+
}
187+
188+
const spec = getControllerSpec(MyController);
189+
expect(spec.paths['/'].get).to.have.property('responses');
190+
expect(spec.paths['/'].get.responses).to.eql({
191+
'200': {
192+
description: 'hello world',
193+
},
194+
});
195+
});
196+
197+
it('generates schema from `x-ts-type`', () => {
198+
class MyController {
199+
@get('/', {
200+
responses: {
201+
'200': {
202+
description: 'hello world',
203+
content: {'application/json': {'x-ts-type': String}},
204+
},
205+
},
206+
})
207+
hello() {
208+
return 'hello world';
209+
}
210+
}
211+
212+
const spec = getControllerSpec(MyController);
213+
expect(spec.paths['/'].get).to.have.property('responses');
214+
expect(spec.paths['/'].get.responses).to.eql({
215+
'200': {
216+
description: 'hello world',
217+
content: {'application/json': {schema: {type: 'string'}}},
218+
},
219+
});
220+
});
221+
222+
it('generates schema for an array from `x-ts-type`', () => {
223+
class MyController {
224+
@get('/', {
225+
responses: {
226+
'200': {
227+
description: 'hello world array',
228+
content: {
229+
'application/json': {
230+
schema: {type: 'array', items: {'x-ts-type': String}},
231+
},
232+
},
233+
},
234+
},
235+
})
236+
hello() {
237+
return ['hello', 'world'];
238+
}
239+
}
240+
241+
const spec = getControllerSpec(MyController);
242+
expect(spec.paths['/'].get).to.have.property('responses');
243+
expect(spec.paths['/'].get.responses).to.eql({
244+
'200': {
245+
description: 'hello world array',
246+
content: {
247+
'application/json': {
248+
schema: {type: 'array', items: {type: 'string'}},
249+
},
250+
},
251+
},
252+
});
253+
});
151254
});

packages/openapi-v3/test/integration/operation-spec.integration.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ describe('operation arguments', () => {
3333
paths: {
3434
'/users/{location}': {
3535
post: {
36-
responses: {},
36+
responses: {
37+
'200': {description: 'Return value of MyController.createUser'},
38+
},
3739
parameters: [
3840
{name: 'type', in: 'query', schema: {type: 'string'}},
3941
{name: 'token', in: 'header', schema: {type: 'string'}},

packages/openapi-v3/test/unit/decorators/operation.decorator.unit.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ describe('Routing metadata', () => {
207207

208208
expect(actualSpec.paths['/greet']['get']).to.eql({
209209
'x-operation-name': 'greet',
210-
responses: {},
210+
responses: {'200': {description: 'Return value of MyController.greet'}},
211211
});
212212
});
213213

@@ -221,7 +221,9 @@ describe('Routing metadata', () => {
221221

222222
expect(actualSpec.paths['/greeting']['post']).to.eql({
223223
'x-operation-name': 'createGreeting',
224-
responses: {},
224+
responses: {
225+
'200': {description: 'Return value of MyController.createGreeting'},
226+
},
225227
});
226228
});
227229

packages/openapi-v3/test/unit/decorators/param/param.decorator.unit.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ describe('Routing metadata for parameters', () => {
3333
const expectedSpec = anOperationSpec()
3434
.withOperationName('greet')
3535
.withParameter(paramSpec)
36+
.withResponse(200, {description: 'Return value of MyController.greet'})
3637
.build();
3738
expect(actualSpec.paths['/greet']['get']).to.eql(expectedSpec);
3839
});
@@ -75,6 +76,7 @@ describe('Routing metadata for parameters', () => {
7576

7677
const expectedSpec = anOperationSpec()
7778
.withOperationName('update')
79+
.withResponse(200, {description: 'Return value of MyController.update'})
7880
.withParameter({
7981
name: 'id',
8082
schema: {
@@ -143,6 +145,7 @@ describe('Routing metadata for parameters', () => {
143145

144146
const expectedSpec = anOperationSpec()
145147
.withOperationName('greet')
148+
.withResponse(200, {description: 'Return value of MyController.greet'})
146149
.withParameter({
147150
name: 'names',
148151
schema: {
@@ -188,6 +191,7 @@ describe('Routing metadata for parameters', () => {
188191

189192
const expectedSpec = anOperationSpec()
190193
.withOperationName('greet')
194+
.withResponse(200, {description: 'Return value of MyController.greet'})
191195
.withParameter({
192196
name: 'names',
193197
schema: {

0 commit comments

Comments
 (0)