Skip to content

Commit

Permalink
Merge 3fd5bae into 377fa08
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Dec 9, 2018
2 parents 377fa08 + 3fd5bae commit 2a44f7e
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 32 deletions.
4 changes: 3 additions & 1 deletion docs/site/OpenAPI-generator.md
Expand Up @@ -19,7 +19,9 @@ lb4 openapi [<url>] [options]
### Options

- `--url`: URL or file path of the OpenAPI spec.
- `--validate`: Validate the OpenAPI spec. Default: false.
- `--validate`: Validate the OpenAPI spec. Default: `false`.
- `--promote-anonymous-schemas`: Promote anonymous schemas as models classes.
Default: `false`.

### Arguments

Expand Down
9 changes: 9 additions & 0 deletions packages/cli/generators/openapi/index.js
Expand Up @@ -38,6 +38,14 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
default: false,
type: Boolean,
});

this.option('promote-anonymous-schemas', {
description: 'Promote anonymous schemas as models',
required: false,
default: false,
type: Boolean,
});

return super._setupGenerator();
}

Expand Down Expand Up @@ -70,6 +78,7 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
const result = await loadAndBuildSpec(this.url, {
log: this.log,
validate: this.options.validate,
promoteAnonymousSchemas: this.options['promote-anonymous-schemas'],
});
debugJson('OpenAPI spec', result.apiSpec);
Object.assign(this, result);
Expand Down
77 changes: 57 additions & 20 deletions packages/cli/generators/openapi/schema-helper.js
Expand Up @@ -4,7 +4,6 @@
// License text available at https://opensource.org/licenses/MIT

'use strict';
const util = require('util');

const {
isExtension,
Expand Down Expand Up @@ -301,29 +300,13 @@ function generateModelSpecs(apiSpec, options) {

const schemaMapping = (options.schemaMapping = options.schemaMapping || {});

const schemas =
(apiSpec && apiSpec.components && apiSpec.components.schemas) || {};

// First map schema objects to names
for (const s in schemas) {
if (isExtension(s)) continue;
schemaMapping[`#/components/schemas/${s}`] = schemas[s];
const className = titleCase(s);
objectTypeMapping.set(schemas[s], {
description: schemas[s].description || s,
name: s,
className,
fileName: getModelFileName(s),
properties: [],
imports: [],
});
}
registerNamedSchemas(apiSpec, options);

const models = [];
// Generate models from schema objects
for (const s in schemas) {
for (const s in options.schemaMapping) {
if (isExtension(s)) continue;
const schema = schemas[s];
const schema = options.schemaMapping[s];
const model = mapSchemaType(schema, {objectTypeMapping, schemaMapping});
// `model` is `undefined` for primitive types
if (model == null) continue;
Expand All @@ -334,6 +317,58 @@ function generateModelSpecs(apiSpec, options) {
return models;
}

/**
* Register the named schema
* @param {string} schemaName Schema name
* @param {object} schema Schema object
* @param {object} typeRegistry Options for objectTypeMapping & schemaMapping
*/
function registerSchema(schemaName, schema, typeRegistry) {
if (typeRegistry.objectTypeMapping.get(schema)) return;
if (!schemaName) {
// Anonymous schema
if (!typeRegistry.promoteAnonymousSchemas) {
// Skip anonymous schemas
return;
}
// Infer a schema name
if (schema.title) {
schemaName = camelCase(schema.title);
} else {
if (typeRegistry.nameIndex == null) typeRegistry.nameIndex = 0;
typeRegistry.nameIndex++;
schemaName = 'AnonymousType_' + typeRegistry.nameIndex;
}
}
typeRegistry.schemaMapping[`#/components/schemas/${schemaName}`] = schema;
const className = titleCase(schemaName);
typeRegistry.objectTypeMapping.set(schema, {
description: schema.description || schemaName,
name: schemaName,
className,
fileName: getModelFileName(schemaName),
properties: [],
imports: [],
});
}

/**
* Register spec.components.schemas
* @param {*} apiSpec OpenAPI spec
* @param {*} typeRegistry options for objectTypeMapping & schemaMapping
*/
function registerNamedSchemas(apiSpec, typeRegistry) {
const schemas =
(apiSpec && apiSpec.components && apiSpec.components.schemas) || {};

// First map schema objects to names
for (const s in schemas) {
if (isExtension(s)) continue;
const schema = schemas[s];
registerSchema(s, schema, typeRegistry);
}
}

function getModelFileName(modelName) {
let name = modelName;
if (modelName.endsWith('Model')) {
Expand All @@ -344,6 +379,8 @@ function getModelFileName(modelName) {

module.exports = {
mapSchemaType,
registerSchema,
registerNamedSchemas,
generateModelSpecs,
getModelFileName,
};
8 changes: 4 additions & 4 deletions packages/cli/generators/openapi/spec-helper.js
Expand Up @@ -4,11 +4,9 @@
// License text available at https://opensource.org/licenses/MIT

'use strict';
const fs = require('fs');
const util = require('util');

const debug = require('../../lib/debug')('openapi-generator');
const {mapSchemaType} = require('./schema-helper');
const {mapSchemaType, registerSchema} = require('./schema-helper');
const {
isExtension,
titleCase,
Expand All @@ -17,7 +15,6 @@ const {
camelCase,
escapeIdentifier,
escapePropertyOrMethodName,
toJsonStr,
} = require('./utils');

const HTTP_VERBS = [
Expand Down Expand Up @@ -162,6 +159,7 @@ function buildMethodSpec(controllerSpec, op, options) {
} else {
paramNames[name] = 1;
}
registerSchema(undefined, p.schema, options);
const pType = mapSchemaType(p.schema, options);
addImportsForType(pType);
comments.push(`@param ${name} ${p.description || ''}`);
Expand All @@ -184,6 +182,7 @@ function buildMethodSpec(controllerSpec, op, options) {
const content = op.spec.requestBody.content;
const jsonType = content && content['application/json'];
if (jsonType && jsonType.schema) {
registerSchema(undefined, jsonType.schema, options);
bodyType = mapSchemaType(jsonType.schema, options);
addImportsForType(bodyType);
}
Expand Down Expand Up @@ -221,6 +220,7 @@ function buildMethodSpec(controllerSpec, op, options) {
const content = responses[code].content;
const jsonType = content && content['application/json'];
if (jsonType && jsonType.schema) {
registerSchema(undefined, jsonType.schema, options);
returnType = mapSchemaType(jsonType.schema, options);
addImportsForType(returnType);
comments.push(`@returns ${responses[code].description || ''}`);
Expand Down
19 changes: 14 additions & 5 deletions packages/cli/generators/openapi/spec-loader.js
Expand Up @@ -10,7 +10,7 @@ const swagger2openapi = require('swagger2openapi');
const {debugJson} = require('./utils');
const _ = require('lodash');
const {generateControllerSpecs} = require('./spec-helper');
const {generateModelSpecs} = require('./schema-helper');
const {generateModelSpecs, registerNamedSchemas} = require('./schema-helper');

/**
* Load swagger specs from the given url or file path; handle yml or json
Expand Down Expand Up @@ -53,11 +53,20 @@ async function loadSpec(specUrlStr, {log, validate} = {}) {
return spec;
}

async function loadAndBuildSpec(url, {log, validate} = {}) {
async function loadAndBuildSpec(
url,
{log, validate, promoteAnonymousSchemas} = {},
) {
const apiSpec = await loadSpec(url, {log, validate});
const options = {objectTypeMapping: new Map(), schemaMapping: {}};
const modelSpecs = generateModelSpecs(apiSpec, options);
const controllerSpecs = generateControllerSpecs(apiSpec, options);
// First populate the type registry for named schemas
const typeRegistry = {
objectTypeMapping: new Map(),
schemaMapping: {},
promoteAnonymousSchemas,
};
registerNamedSchemas(apiSpec, typeRegistry);
const controllerSpecs = generateControllerSpecs(apiSpec, typeRegistry);
const modelSpecs = generateModelSpecs(apiSpec, typeRegistry);
return {
apiSpec,
modelSpecs,
Expand Down
144 changes: 142 additions & 2 deletions packages/cli/test/unit/openapi/schema-model.unit.js
Expand Up @@ -4,14 +4,17 @@
// License text available at https://opensource.org/licenses/MIT

const expect = require('@loopback/testlab').expect;
const {loadSpec} = require('../../../generators/openapi/spec-loader');
const {
loadSpec,
loadAndBuildSpec,
} = require('../../../generators/openapi/spec-loader');
const {
generateModelSpecs,
} = require('../../../generators/openapi/schema-helper');
const path = require('path');

describe('schema to model', () => {
let usptoSpec, petstoreSpec, customerSepc;
let usptoSpec, usptoSpecAnonymous, petstoreSpec, customerSepc;
const uspto = path.join(__dirname, '../../fixtures/openapi/3.0/uspto.yaml');
const petstore = path.join(
__dirname,
Expand All @@ -24,6 +27,9 @@ describe('schema to model', () => {

before(async () => {
usptoSpec = await loadSpec(uspto);
usptoSpecAnonymous = await loadAndBuildSpec(uspto, {
promoteAnonymousSchemas: true,
});
petstoreSpec = await loadSpec(petstore);
customerSepc = await loadSpec(customer);
});
Expand Down Expand Up @@ -63,6 +69,140 @@ describe('schema to model', () => {
]);
});

it('generates models for uspto with promoted anonymous schemas', () => {
const models = usptoSpecAnonymous.modelSpecs;
expect(models).to.eql([
{
description: 'dataSetList',
name: 'dataSetList',
className: 'DataSetList',
fileName: 'data-set-list.model.ts',
properties: [
{
name: 'total',
signature: 'total?: number;',
decoration: "@property({name: 'total'})",
},
{
name: 'apis',
signature:
'apis?: {\n apiKey?: string;\n apiVersionNumber?: string;\n ' +
'apiUrl?: string;\n apiDocumentationUrl?: string;\n}[];',
decoration: "@property({name: 'apis'})",
},
],
imports: [],
import: "import {DataSetList} from './data-set-list.model';",
kind: 'class',
declaration:
'{\n total?: number;\n apis?: {\n apiKey?: string;\n ' +
'apiVersionNumber?: string;\n apiUrl?: string;\n ' +
'apiDocumentationUrl?: string;\n}[];\n}',
signature: 'DataSetList',
},
{
description: 'dataSetList',
name: 'dataSetList',
className: 'DataSetList',
fileName: 'data-set-list.model.ts',
properties: [
{
name: 'total',
signature: 'total?: number;',
decoration: "@property({name: 'total'})",
},
{
name: 'apis',
signature:
'apis?: {\n apiKey?: string;\n apiVersionNumber?: string;\n ' +
'apiUrl?: string;\n apiDocumentationUrl?: string;\n}[];',
decoration: "@property({name: 'apis'})",
},
],
imports: [],
import: "import {DataSetList} from './data-set-list.model';",
kind: 'class',
declaration:
'{\n total?: number;\n apis?: {\n apiKey?: string;\n ' +
'apiVersionNumber?: string;\n apiUrl?: string;\n ' +
'apiDocumentationUrl?: string;\n}[];\n}',
signature: 'DataSetList',
},
{
description: 'AnonymousType_2',
name: 'AnonymousType_2',
className: 'AnonymousType2',
fileName: 'anonymous-type-2.model.ts',
properties: [],
imports: [],
import: "import {AnonymousType2} from './anonymous-type-2.model';",
declaration: 'string',
signature: 'AnonymousType2',
},
{
description: 'AnonymousType_3',
name: 'AnonymousType_3',
className: 'AnonymousType3',
fileName: 'anonymous-type-3.model.ts',
properties: [],
imports: [],
import: "import {AnonymousType3} from './anonymous-type-3.model';",
declaration: 'string',
signature: 'AnonymousType3',
},
{
description: 'AnonymousType_4',
name: 'AnonymousType_4',
className: 'AnonymousType4',
fileName: 'anonymous-type-4.model.ts',
properties: [],
imports: [],
import: "import {AnonymousType4} from './anonymous-type-4.model';",
declaration: 'string',
signature: 'AnonymousType4',
},
{
description: 'AnonymousType_5',
name: 'AnonymousType_5',
className: 'AnonymousType5',
fileName: 'anonymous-type-5.model.ts',
properties: [],
imports: [],
import: "import {AnonymousType5} from './anonymous-type-5.model';",
declaration: 'string',
signature: 'AnonymousType5',
},
{
description: 'AnonymousType_6',
name: 'AnonymousType_6',
className: 'AnonymousType6',
fileName: 'anonymous-type-6.model.ts',
properties: [],
imports: [],
import: "import {AnonymousType6} from './anonymous-type-6.model';",
declaration: 'string',
signature: 'AnonymousType6',
},
{
description: 'AnonymousType_7',
name: '{\n \n}[]',
className: 'AnonymousType7',
fileName: 'anonymous-type-7.model.ts',
properties: [],
imports: [],
import: "import {AnonymousType7} from './anonymous-type-7.model';",
declaration: '{\n \n}[]',
signature: 'AnonymousType7',
itemType: {
imports: [],
declaration: '{\n \n}',
properties: [],
signature: '{\n \n}',
},
},
]);
});

it('generates models for petstore', () => {
const objectTypeMapping = new Map();
const models = generateModelSpecs(petstoreSpec, {objectTypeMapping});
Expand Down

0 comments on commit 2a44f7e

Please sign in to comment.