Skip to content

Commit

Permalink
Merge 6594975 into 9de1133
Browse files Browse the repository at this point in the history
  • Loading branch information
mschnee committed Jan 25, 2020
2 parents 9de1133 + 6594975 commit 33df0c3
Show file tree
Hide file tree
Showing 2 changed files with 305 additions and 16 deletions.
257 changes: 257 additions & 0 deletions packages/openapi-v3/src/__tests__/unit/x-ts-type.unit.ts
@@ -0,0 +1,257 @@
// Copyright IBM Corp. 2019. All Rights Reserved.
// Node module: @loopback/openapi-v3
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
import {Model, model, property} from '@loopback/repository';
import {expect} from '@loopback/testlab';
import {RequestBodyObject, ResponseObject} from 'openapi3-ts';
import {getControllerSpec} from '../..';
import {get, post, requestBody} from '../../decorators';

describe('x-ts-type is converted in the right places', () => {
// setup the models for use
@model()
class TestRequest extends Model {
@property({default: 1})
value: number;
}
@model()
class SuccessModel extends Model {
constructor(err: Partial<SuccessModel>) {
super(err);
}
@property({default: 'ok'})
message: string;
}

@model()
class FooError extends Model {
constructor(err: Partial<FooError>) {
super(err);
}
@property({default: 'foo'})
foo: string;
}

@model()
class NotError extends Model {
constructor(err: Partial<NotError>) {
super(err);
}
@property({default: true})
fail: boolean;
}

@model()
class BarError extends Model {
constructor(err: Partial<BarError>) {
super(err);
}
@property({default: 'bar'})
bar: string;
}

const testRequestSchema: RequestBodyObject = {
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/TestRequest',
},
},
},
};
const successSchema: ResponseObject = {
description: 'Success',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/SuccessModel',
},
},
},
};

const notSchema: ResponseObject = {
description: 'Failure',
content: {
'application/json': {
schema: {
not: {$ref: '#/components/schemas/BarError'},
},
},
},
};
const fooBarSchema = (k: 'anyOf' | 'allOf' | 'oneOf'): ResponseObject => ({
description: 'Failure',
content: {
'application/json': {
schema: {
[k]: [
{$ref: '#/components/schemas/FooError'},
{$ref: '#/components/schemas/BarError'},
],
not: {$ref: '#/components/schemas/NotError'},
},
},
},
});

it('Allows a simple request schema', () => {
class MyController {
@post('/greet')
greet(@requestBody() body: TestRequest) {
return 'Hello world!';
}
}
const actualSpec = getControllerSpec(MyController);
expect(actualSpec.paths['/greet'].post.requestBody).to.eql(
testRequestSchema,
);
});

it('Does not process existing $ref responses', () => {
const successContent = {$ref: '#/components/schema/SomeReference'};
class MyController {
@post('/greet', {
responses: {
201: successContent,
},
})
greet(@requestBody() body: TestRequest) {
return 'Hello world!';
}
}
const actualSpec = getControllerSpec(MyController);
expect(actualSpec.paths['/greet'].post.responses[201]).to.eql(
successContent,
);
});

it('Allows for a response schema using the spec', () => {
class MyController {
@get('/greet', {
responses: {
200: {
description: 'Success',
content: {
'application/json': {
schema: {
'x-ts-type': SuccessModel,
},
},
},
},
},
})
greet() {
return new SuccessModel({message: 'hello, world'});
}
}
const actualSpec = getControllerSpec(MyController);
expect(actualSpec.paths['/greet'].get.responses[200]).to.eql(successSchema);
expect(actualSpec.components?.schemas?.SuccessModel).to.not.be.undefined();
});

it('Allows `anyOf` responses', () => {
class MyController {
@get('/greet', {
responses: {
404: {
description: 'Failure',
content: {
'application/json': {
schema: {
anyOf: [{'x-ts-type': FooError}, {'x-ts-type': BarError}],
not: {'x-ts-type': NotError},
},
},
},
},
},
})
greet() {
throw new FooError({foo: 'foo'});
}
}
const actualSpec = getControllerSpec(MyController);
expect(actualSpec.paths['/greet'].get.responses[404]).to.eql(
fooBarSchema('anyOf'),
);
});
it('Allows `allOf` responses', () => {
class MyController {
@get('/greet', {
responses: {
404: {
description: 'Failure',
content: {
'application/json': {
schema: {
allOf: [{'x-ts-type': FooError}, {'x-ts-type': BarError}],
not: {'x-ts-type': NotError},
},
},
},
},
},
})
greet() {
throw new FooError({foo: 'foo'});
}
}
const actualSpec = getControllerSpec(MyController);
expect(actualSpec.paths['/greet'].get.responses[404]).to.eql(
fooBarSchema('allOf'),
);
});

it('Allows `oneOf` responses', () => {
class MyController {
@get('/greet', {
responses: {
404: {
description: 'Failure',
content: {
'application/json': {
schema: {
oneOf: [{'x-ts-type': FooError}, {'x-ts-type': BarError}],
not: {'x-ts-type': NotError},
},
},
},
},
},
})
greet() {
throw new FooError({foo: 'foo'});
}
}
const actualSpec = getControllerSpec(MyController);
expect(actualSpec.paths['/greet'].get.responses[404]).to.eql(
fooBarSchema('oneOf'),
);
});
it('Allows `not` responses', () => {
class MyController {
@get('/greet', {
responses: {
404: {
description: 'Failure',
content: {
'application/json': {
schema: {
not: {'x-ts-type': BarError},
},
},
},
},
},
})
greet() {
throw new FooError({foo: 'foo'});
}
}
const actualSpec = getControllerSpec(MyController);
expect(actualSpec.paths['/greet'].get.responses[404]).to.eql(notSchema);
});
});
64 changes: 48 additions & 16 deletions packages/openapi-v3/src/controller-spec.ts
Expand Up @@ -178,9 +178,11 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {

requestBody = requestBodies[0];
debug(' requestBody for method %s: %j', op, requestBody);
/* istanbul ignore else */
if (requestBody) {
operationSpec.requestBody = requestBody;

/* istanbul ignore else */
const content = requestBody.content || {};
for (const mediaType in content) {
processSchemaExtensions(spec, content[mediaType].schema);
Expand Down Expand Up @@ -233,6 +235,9 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {
return spec;
}

declare type MixKey = 'allOf' | 'anyOf' | 'oneOf';
const SCHEMA_ARR_KEYS: MixKey[] = ['allOf', 'anyOf', 'oneOf'];

/**
* Resolve the x-ts-type in the schema object
* @param spec - Controller spec
Expand All @@ -248,24 +253,51 @@ function processSchemaExtensions(
assignRelatedSchemas(spec, schema.definitions);
delete schema.definitions;

if (isReferenceObject(schema)) return;
/**
* check if we have been provided a `not`
* `not` is valid in many cases- here we're checking for
* `not: { schema: {'x-ts-type': SomeModel }}
*/
if (schema.not) {
processSchemaExtensions(spec, schema.not);
}

/**
* check for schema.allOf, schema.oneOf, schema.anyOf arrays first.
* You cannot provide BOTH a defnintion AND one of these keywords.
*/
/* istanbul ignore else */
const hasOwn = (prop: string) => schema?.hasOwnProperty(prop);

if (SCHEMA_ARR_KEYS.some(k => hasOwn(k))) {
SCHEMA_ARR_KEYS.forEach((k: MixKey) => {
/* istanbul ignore else */
if (schema?.[k] && Array.isArray(schema[k])) {
schema[k].forEach((r: (SchemaObject | ReferenceObject)[]) => {
processSchemaExtensions(spec, r);
});
}
});
} else {
if (isReferenceObject(schema)) return;

const tsType = schema[TS_TYPE_KEY];
debug(' %s => %o', TS_TYPE_KEY, tsType);
if (tsType) {
schema = resolveSchema(tsType, schema);
if (schema.$ref) generateOpenAPISchema(spec, tsType);
const tsType = schema[TS_TYPE_KEY];
debug(' %s => %o', TS_TYPE_KEY, tsType);
if (tsType) {
schema = resolveSchema(tsType, schema);
if (schema.$ref) generateOpenAPISchema(spec, tsType);

// We don't want a Function type in the final spec.
delete schema[TS_TYPE_KEY];
return;
}
if (schema.type === 'array') {
processSchemaExtensions(spec, schema.items);
} else if (schema.type === 'object') {
if (schema.properties) {
for (const p in schema.properties) {
processSchemaExtensions(spec, schema.properties[p]);
// We don't want a Function type in the final spec.
delete schema[TS_TYPE_KEY];
return;
}
if (schema.type === 'array') {
processSchemaExtensions(spec, schema.items);
} else if (schema.type === 'object') {
if (schema.properties) {
for (const p in schema.properties) {
processSchemaExtensions(spec, schema.properties[p]);
}
}
}
}
Expand Down

0 comments on commit 33df0c3

Please sign in to comment.