Skip to content

Commit

Permalink
feat(openapi-v3): allow controller to reference models via openapispec
Browse files Browse the repository at this point in the history
Controller methods can now reference model schema through OpenAPI spec without the need to use the `x-ts-type` extension.

Co-authored-by: Miroslav Bajtoš <[email protected]>
  • Loading branch information
nabdelgadir and bajtos committed May 14, 2019
1 parent 6fe956e commit 7e75867
Show file tree
Hide file tree
Showing 2 changed files with 189 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {ParameterObject, SchemaObject} from '@loopback/openapi-v3-types';
import {
OperationObject,
ParameterObject,
SchemaObject,
} from '@loopback/openapi-v3-types';
import {model, property} from '@loopback/repository';
import {expect} from '@loopback/testlab';
import {
Expand Down Expand Up @@ -203,6 +207,131 @@ describe('controller spec', () => {
});
});

it('allows operations to provide definition of referenced models through #/components/schema', () => {
class MyController {
@get('/todos', {
responses: {
'200': {
description: 'Array of Category model instances',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Todo',
definitions: {
Todo: {
title: 'Todo',
properties: {
title: {type: 'string'},
},
},
},
},
},
},
},
},
})
async find(): Promise<object[]> {
return []; // dummy implementation, it's never called
}
}

const spec = getControllerSpec(MyController);
const opSpec: OperationObject = spec.paths['/todos'].get;
const responseSpec = opSpec.responses['200'].content['application/json'];
expect(responseSpec.schema).to.deepEqual({
$ref: '#/components/schemas/Todo',
});

const globalSchemas = (spec.components || {}).schemas;
expect(globalSchemas).to.deepEqual({
Todo: {
title: 'Todo',
properties: {
title: {
type: 'string',
},
},
},
});
});

it('allows operations to provide definition of referenced models through #/definitions', () => {
class MyController {
@get('/todos', {
responses: {
'200': {
description: 'Array of Category model instances',
content: {
'application/json': {
schema: {
$ref: '#/definitions/Todo',
definitions: {
Todo: {
title: 'Todo',
properties: {
title: {type: 'string'},
},
},
},
},
},
},
},
},
})
async find(): Promise<object[]> {
return []; // dummy implementation, it's never called
}
}

const spec = getControllerSpec(MyController);
const opSpec: OperationObject = spec.paths['/todos'].get;
const responseSpec = opSpec.responses['200'].content['application/json'];
expect(responseSpec.schema).to.deepEqual({
$ref: '#/definitions/Todo',
});

const globalSchemas = (spec.components || {}).schemas;
expect(globalSchemas).to.deepEqual({
Todo: {
title: 'Todo',
properties: {
title: {
type: 'string',
},
},
},
});
});

it('returns an empty object when it cannot find definition of referenced model', () => {
class MyController {
@get('/todos', {
responses: {
'200': {
description: 'Array of Category model instances',
content: {
'application/json': {
schema: {
$ref: '#/definitions/Todo',
definitions: {},
},
},
},
},
},
})
async find(): Promise<object[]> {
return []; // dummy implementation, it's never called
}
}

const spec = getControllerSpec(MyController);
const globalSchemas = (spec.components || {}).schemas;
expect(globalSchemas).to.be.empty();
});

describe('x-ts-type', () => {
@model()
class MyModel {
Expand Down
87 changes: 59 additions & 28 deletions packages/openapi-v3/src/controller-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,24 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {MetadataInspector, DecoratorFactory} from '@loopback/context';

import {DecoratorFactory, MetadataInspector} from '@loopback/context';
import {
ComponentsObject,
ISpecificationExtension,
isReferenceObject,
OperationObject,
ParameterObject,
PathObject,
ComponentsObject,
ReferenceObject,
RequestBodyObject,
ResponseObject,
ReferenceObject,
SchemaObject,
isReferenceObject,
} from '@loopback/openapi-v3-types';
import {getJsonSchema} from '@loopback/repository-json-schema';
import {OAI3Keys} from './keys';
import {jsonToSchemaObject} from './json-to-schema';
import * as _ from 'lodash';
import {resolveSchema} from './generate-schema';
import {jsonToSchemaObject} from './json-to-schema';
import {OAI3Keys} from './keys';

const debug = require('debug')('loopback:openapi3:metadata:controller-spec');

Expand Down Expand Up @@ -56,6 +56,8 @@ export interface RestEndpoint {

export const TS_TYPE_KEY = 'x-ts-type';

type ComponentSchemaMap = {[key: string]: SchemaObject};

/**
* Build the api spec from class and method level decorations
* @param constructor Controller class
Expand Down Expand Up @@ -120,8 +122,8 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {
if (isReferenceObject(responseObject)) continue;
const content = responseObject.content || {};
for (const c in content) {
debug(' evaluating response code %s with content: %o', code, c);
resolveTSType(spec, content[c].schema);
debug(' processing response code %s with content-type %', code, c);
processSchemaExtensions(spec, content[c].schema);
}
}

Expand Down Expand Up @@ -180,7 +182,7 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {

const content = requestBody.content || {};
for (const mediaType in content) {
resolveTSType(spec, content[mediaType].schema);
processSchemaExtensions(spec, content[mediaType].schema);
}
}
}
Expand Down Expand Up @@ -235,12 +237,18 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {
* @param spec Controller spec
* @param schema Schema object
*/
function resolveTSType(
function processSchemaExtensions(
spec: ControllerSpec,
schema?: SchemaObject | ReferenceObject,
schema?: SchemaObject | (ReferenceObject & ISpecificationExtension),
) {
debug(' evaluating schema: %j', schema);
if (!schema || isReferenceObject(schema)) return;
debug(' processing extensions in schema: %j', schema);
if (!schema) return;

assignRelatedSchemas(spec, schema.definitions);
delete schema.definitions;

if (isReferenceObject(schema)) return;

const tsType = schema[TS_TYPE_KEY];
debug(' %s => %o', TS_TYPE_KEY, tsType);
if (tsType) {
Expand All @@ -252,11 +260,11 @@ function resolveTSType(
return;
}
if (schema.type === 'array') {
resolveTSType(spec, schema.items);
processSchemaExtensions(spec, schema.items);
} else if (schema.type === 'object') {
if (schema.properties) {
for (const p in schema.properties) {
resolveTSType(spec, schema.properties[p]);
processSchemaExtensions(spec, schema.properties[p]);
}
}
}
Expand All @@ -281,20 +289,43 @@ function generateOpenAPISchema(spec: ControllerSpec, tsType: Function) {
}
const jsonSchema = getJsonSchema(tsType);
const openapiSchema = jsonToSchemaObject(jsonSchema);
const outputSchemas = spec.components.schemas;
if (openapiSchema.definitions) {
for (const key in openapiSchema.definitions) {
// Preserve user-provided definitions
if (key in outputSchemas) continue;
const relatedSchema = openapiSchema.definitions[key];
debug(' defining referenced schema for %j: %j', key, relatedSchema);
outputSchemas[key] = relatedSchema;
}
delete openapiSchema.definitions;
}

assignRelatedSchemas(spec, openapiSchema.definitions);
delete openapiSchema.definitions;

debug(' defining schema for %j: %j', tsType.name, openapiSchema);
outputSchemas[tsType.name] = openapiSchema;
spec.components.schemas[tsType.name] = openapiSchema;
}

/**
* Assign related schemas from definitions to the controller spec
* @param spec Controller spec
* @param definitions Schema definitions
*/
function assignRelatedSchemas(
spec: ControllerSpec,
definitions?: ComponentSchemaMap,
) {
if (!definitions) return;
debug(
' assigning related schemas: ',
definitions && Object.keys(definitions),
);
if (!spec.components) {
spec.components = {};
}
if (!spec.components.schemas) {
spec.components.schemas = {};
}
const outputSchemas = spec.components.schemas;

for (const key in definitions) {
// Preserve user-provided definitions
if (key in outputSchemas) continue;
const relatedSchema = definitions[key];
debug(' defining referenced schema for %j: %j', key, relatedSchema);
outputSchemas[key] = relatedSchema;
}
}

/**
Expand Down

0 comments on commit 7e75867

Please sign in to comment.