Skip to content

Commit 640b941

Browse files
committed
feat(cli): improve openapi handling of body and impl
1 parent 08c2d89 commit 640b941

File tree

10 files changed

+135
-25
lines changed

10 files changed

+135
-25
lines changed

packages/cli/generators/openapi/index.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
108108
if (this.shouldExit()) return false;
109109
this._generateModels();
110110
this._generateControllers();
111+
await this._updateIndex();
111112
}
112113

113114
_generateControllers() {
@@ -145,9 +146,7 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
145146
}
146147
}
147148

148-
async end() {
149-
await super.end();
150-
if (this.shouldExit()) return;
149+
async _updateIndex() {
151150
const targetDir = this.destinationPath(`src/controllers`);
152151
for (const c of this.selectedControllers) {
153152
// Check all files being generated to ensure they succeeded
@@ -157,4 +156,9 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
157156
}
158157
}
159158
}
159+
160+
async end() {
161+
await super.end();
162+
if (this.shouldExit()) return;
163+
}
160164
};

packages/cli/generators/openapi/schema-helper.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,15 @@
55

66
'use strict';
77
const util = require('util');
8-
const json5 = require('json5');
98

109
const {
1110
isExtension,
1211
titleCase,
1312
kebabCase,
1413
escapePropertyOrMethodName,
14+
toJsonStr,
1515
} = require('./utils');
1616

17-
function toJsonStr(val) {
18-
return json5.stringify(val, null, 2);
19-
}
20-
2117
function setImport(typeSpec) {
2218
if (typeSpec.fileName) {
2319
typeSpec.import = `import {${typeSpec.className}} from './${getBaseName(
@@ -165,10 +161,17 @@ function mapObjectType(schema, options) {
165161
);
166162
// The property name might have chars such as `-`
167163
const propName = escapePropertyOrMethodName(p);
164+
let propDecoration = `@property({name: '${p}'})`;
165+
if (propertyType.itemType && propertyType.itemType.kind === 'class') {
166+
// Use `@property.array` for array types
167+
propDecoration = `@property.array(${
168+
propertyType.itemType.className
169+
}, {name: '${p}'})`;
170+
}
168171
const propSpec = {
169172
name: p,
170173
signature: `${propName + suffix}: ${propertyType.signature};`,
171-
decoration: `@property({name: '${p}'})`,
174+
decoration: propDecoration,
172175
};
173176
if (schema.properties[p].description) {
174177
propSpec.description = schema.properties[p].description;

packages/cli/generators/openapi/spec-helper.js

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const {
1717
camelCase,
1818
escapeIdentifier,
1919
escapePropertyOrMethodName,
20+
toJsonStr,
2021
} = require('./utils');
2122

2223
const HTTP_VERBS = [
@@ -150,9 +151,9 @@ function buildMethodSpec(controllerSpec, op, options) {
150151
const methodName = getMethodName(op.spec);
151152
let args = [];
152153
const parameters = op.spec.parameters;
154+
// Keep track of param names to avoid duplicates
155+
const paramNames = {};
153156
if (parameters) {
154-
// Keep track of param names to avoid duplicates
155-
const paramNames = {};
156157
args = parameters.map(p => {
157158
const name = escapeIdentifier(p.name);
158159
if (name in paramNames) {
@@ -167,7 +168,35 @@ function buildMethodSpec(controllerSpec, op, options) {
167168
}`;
168169
});
169170
}
170-
let returnType = 'any';
171+
if (op.spec.requestBody) {
172+
/**
173+
* requestBody:
174+
* description: Pet to add to the store
175+
* required: true
176+
* content:
177+
* application/json:
178+
* schema:
179+
* $ref: '#/components/schemas/NewPet'
180+
*/
181+
let bodyType = {signature: 'any'};
182+
const content = op.spec.requestBody.content;
183+
const jsonType = content && content['application/json'];
184+
if (jsonType && jsonType.schema) {
185+
bodyType = mapSchemaType(jsonType.schema, options);
186+
addImportsForType(bodyType);
187+
}
188+
let bodyName = 'body';
189+
if (bodyName in paramNames) {
190+
bodyName = `${bodyName}${paramNames[bodyName]++}`;
191+
}
192+
const bodyParam = bodyName; // + (op.spec.requestBody.required ? '' : '?');
193+
// Add body as the 1st param
194+
const bodySpec = ''; // toJsonStr(op.spec.requestBody);
195+
args.unshift(
196+
`@requestBody(${bodySpec}) ${bodyParam}: ${bodyType.signature}`,
197+
);
198+
}
199+
let returnType = {signature: 'any'};
171200
const responses = op.spec.responses;
172201
if (responses) {
173202
/**
@@ -195,11 +224,15 @@ function buildMethodSpec(controllerSpec, op, options) {
195224
const signature = `async ${methodName}(${args.join(', ')}): Promise<${
196225
returnType.signature
197226
}>`;
198-
return {
227+
const methodSpec = {
199228
description: op.spec.description,
200229
decoration: `@operation('${op.verb}', '${op.path}')`,
201230
signature,
202231
};
232+
if (op.spec['x-implementation']) {
233+
methodSpec.implementation = op.spec['x-implementation'];
234+
}
235+
return methodSpec;
203236

204237
function addImportsForType(typeSpec) {
205238
if (typeSpec.className && typeSpec.import) {

packages/cli/generators/openapi/templates/src/controllers/controller-template.ts.ejs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* tslint:disable:no-any */
2-
import {operation, param} from '@loopback/rest';
2+
import {operation, param, requestBody} from '@loopback/rest';
33
<%_
44
imports.forEach(i => {
55
-%>
@@ -22,7 +22,7 @@ export class <%- className %> {
2222
*/
2323
<%- m.decoration %>
2424
<%- m.signature %> {
25-
throw new Error('Not implemented');
25+
<%- m.implementation || "throw new Error('Not implemented');" %>
2626
}
2727
2828
<%_ } -%>

packages/cli/generators/openapi/utils.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
const fs = require('fs');
88
const util = require('util');
99
const _ = require('lodash');
10+
const json5 = require('json5');
1011

1112
const utils = require('../../lib/utils');
1213
const debug = require('../../lib/debug')('openapi-generator');
@@ -168,6 +169,10 @@ function escapePropertyOrMethodName(name) {
168169
return name;
169170
}
170171

172+
function toJsonStr(val) {
173+
return json5.stringify(val, null, 2);
174+
}
175+
171176
module.exports = {
172177
isExtension,
173178
titleCase,
@@ -177,4 +182,5 @@ module.exports = {
177182
camelCase: _.camelCase,
178183
escapeIdentifier,
179184
escapePropertyOrMethodName,
185+
toJsonStr,
180186
};

packages/cli/smoke-test/openapi/code-gen-utils.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ function runNpmTest(sandbox) {
5757
return runNpmScript(sandbox, ['test']);
5858
}
5959

60-
function runPrettier(sandbox) {
61-
return runNpmScript(sandbox, ['run', 'prettier:fix']);
60+
function runLintFix(sandbox) {
61+
return runNpmScript(sandbox, ['run', 'lint:fix']);
6262
}
6363

6464
function runNpmScript(sandbox, args) {
@@ -95,5 +95,5 @@ module.exports = {
9595
generateOpenApiArtifacts,
9696
cleanSandbox,
9797
runNpmTest,
98-
runPrettier,
98+
runLintFix,
9999
};

packages/cli/smoke-test/openapi/real-world-apis.smoke.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const {
1313
createAppProject,
1414
generateOpenApiArtifacts,
1515
cleanSandbox,
16-
runPrettier,
16+
runLintFix,
1717
runNpmTest,
1818
} = require('./code-gen-utils');
1919

@@ -91,7 +91,7 @@ describe('Real-world APIs', () => {
9191
console.log('Sandbox app created: %s', sandbox);
9292
await generateOpenApiArtifacts(sandbox, api.swaggerUrl);
9393
console.log('OpenApi artifacts generated');
94-
await runPrettier(sandbox);
94+
await runLintFix(sandbox);
9595
console.log('Artifacts formatted with prettier');
9696
await runNpmTest(sandbox);
9797
console.log('npm test is passing', sandbox);

packages/cli/test/fixtures/openapi/3.0/customer.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ paths:
2222
tags:
2323
- Customer
2424
description: Returns all customers
25+
operationId: getCustomers
2526
parameters:
2627
- name: if
2728
in: query
@@ -73,6 +74,7 @@ paths:
7374
tags:
7475
- Customer
7576
description: Returns a customer based on a single ID
77+
x-implementation: "return {id: id, 'first-name': 'John', last-name: 'Smith'};"
7678
operationId: find customer by id
7779
parameters:
7880
- name: id
@@ -93,6 +95,17 @@ components:
9395
schemas:
9496
Name:
9597
type: string
98+
Address:
99+
type: object
100+
properties:
101+
street:
102+
type: string
103+
city:
104+
type: string
105+
state:
106+
type: string
107+
zipCode:
108+
type: string
96109
Customer:
97110
required:
98111
- id
@@ -104,5 +117,9 @@ components:
104117
type: string
105118
last-name:
106119
$ref: '#/components/schemas/Name'
120+
addresses:
121+
type: array
122+
items:
123+
$ref: '#/components/schemas/Address'
107124

108125

packages/cli/test/unit/openapi/controller-spec.unit.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ describe('openapi to controllers/models', () => {
2626
description: 'Returns all customers',
2727
decoration: "@operation('get', '/customers')",
2828
signature:
29-
"async ''(@param({name: 'if', in: 'query'}) _if: string[], " +
30-
"@param({name: 'limit', in: 'query'}) limit: number, " +
29+
"async getCustomers(@param({name: 'if', in: 'query'}) _if: " +
30+
"string[], @param({name: 'limit', in: 'query'}) limit: number, " +
3131
"@param({name: 'access-token', in: 'query'}) " +
3232
'accessToken: string): ' +
3333
'Promise<Customer[]>',
@@ -36,12 +36,15 @@ describe('openapi to controllers/models', () => {
3636
description: 'Creates a new customer',
3737
decoration: "@operation('post', '/customers')",
3838
signature:
39-
"async createCustomer(@param({name: 'access-token', " +
39+
'async createCustomer(@requestBody() body: Customer, ' +
40+
"@param({name: 'access-token', " +
4041
"in: 'query'}) accessToken: string): Promise<Customer>",
4142
},
4243
{
4344
description: 'Returns a customer based on a single ID',
4445
decoration: "@operation('get', '/customers/{id}')",
46+
implementation:
47+
"return {id: id, 'first-name': 'John', last-name: 'Smith'};",
4548
signature:
4649
"async findCustomerById(@param({name: 'id', in: 'path'}) " +
4750
'id: number): Promise<Customer>',

packages/cli/test/unit/openapi/schema-model.unit.js

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,41 @@ describe('schema to model', () => {
179179
declaration: 'string',
180180
signature: 'Name',
181181
},
182+
{
183+
description: 'Address',
184+
name: 'Address',
185+
className: 'Address',
186+
fileName: 'address.model.ts',
187+
properties: [
188+
{
189+
name: 'street',
190+
signature: 'street?: string;',
191+
decoration: "@property({name: 'street'})",
192+
},
193+
{
194+
name: 'city',
195+
signature: 'city?: string;',
196+
decoration: "@property({name: 'city'})",
197+
},
198+
{
199+
name: 'state',
200+
signature: 'state?: string;',
201+
decoration: "@property({name: 'state'})",
202+
},
203+
{
204+
name: 'zipCode',
205+
signature: 'zipCode?: string;',
206+
decoration: "@property({name: 'zipCode'})",
207+
},
208+
],
209+
imports: [],
210+
import: "import {Address} from './address.model';",
211+
kind: 'class',
212+
declaration:
213+
'{\n street?: string;\n city?: string;\n state?: string;\n' +
214+
' zipCode?: string;\n}',
215+
signature: 'Address',
216+
},
182217
{
183218
description: 'Customer',
184219
name: 'Customer',
@@ -200,12 +235,21 @@ describe('schema to model', () => {
200235
signature: "'last-name'?: Name;",
201236
decoration: "@property({name: 'last-name'})",
202237
},
238+
{
239+
name: 'addresses',
240+
signature: 'addresses?: Address[];',
241+
decoration: "@property.array(Address, {name: 'addresses'})",
242+
},
243+
],
244+
imports: [
245+
"import {Name} from './name.model';",
246+
"import {Address} from './address.model';",
203247
],
204-
imports: ["import {Name} from './name.model';"],
205248
import: "import {Customer} from './customer.model';",
206249
kind: 'class',
207250
declaration:
208-
"{\n id: number;\n 'first-name'?: string;\n 'last-name'?: Name;\n}",
251+
"{\n id: number;\n 'first-name'?: string;\n " +
252+
"'last-name'?: Name;\n addresses?: Address[];\n}",
209253
signature: 'Customer',
210254
},
211255
]);

0 commit comments

Comments
 (0)