Skip to content

Commit 245f39c

Browse files
authored
Merge pull request lukeautry#643 from Kiwup/toJSON
feat: ignore class methods
2 parents 82d0273 + 1386975 commit 245f39c

File tree

9 files changed

+223
-4
lines changed

9 files changed

+223
-4
lines changed

src/metadataGeneration/typeResolver.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ export class TypeResolver {
144144
if (ts.isMappedTypeNode(this.typeNode) && this.referencer) {
145145
const isNotIgnored = (e: ts.Declaration) => {
146146
const ignore = isExistJSDocTag(e, tag => tag.tagName.text === 'ignore');
147-
return !ignore;
147+
return !ignore && (ts.isPropertyDeclaration(e) || ts.isPropertySignature(e) || ts.isParameter(e));
148148
};
149149

150150
const type = this.current.typeChecker.getTypeFromTypeNode(this.referencer);
@@ -477,15 +477,42 @@ export class TypeResolver {
477477
}
478478

479479
private getModelReference(modelType: ts.InterfaceDeclaration | ts.ClassDeclaration, name: string) {
480+
const example = this.getNodeExample(modelType);
481+
const description = this.getNodeDescription(modelType);
482+
483+
// Handle toJSON methods
484+
if (!modelType.name) {
485+
throw new GenerateMetadataError("Can't get Symbol from anonymous class", modelType);
486+
}
487+
const type = this.current.typeChecker.getTypeAtLocation(modelType.name);
488+
const toJSON = this.current.typeChecker.getPropertyOfType(type, 'toJSON');
489+
if (toJSON && toJSON.valueDeclaration && (ts.isMethodDeclaration(toJSON.valueDeclaration) || ts.isMethodSignature(toJSON.valueDeclaration))) {
490+
let nodeType = toJSON.valueDeclaration.type;
491+
if (!nodeType) {
492+
const signature = this.current.typeChecker.getSignatureFromDeclaration(toJSON.valueDeclaration);
493+
const implicitType = this.current.typeChecker.getReturnTypeOfSignature(signature!);
494+
nodeType = this.current.typeChecker.typeToTypeNode(implicitType) as ts.TypeNode;
495+
}
496+
const type = new TypeResolver(nodeType, this.current).resolve();
497+
const referenceType: Tsoa.ReferenceType = {
498+
refName: this.getRefTypeName(name),
499+
dataType: 'refAlias',
500+
description,
501+
type,
502+
validators: {},
503+
...(example && { example }),
504+
};
505+
return referenceType;
506+
}
507+
480508
const properties = this.getModelProperties(modelType);
481509
const additionalProperties = this.getModelAdditionalProperties(modelType);
482510
const inheritedProperties = this.getModelInheritedProperties(modelType) || [];
483-
const example = this.getNodeExample(modelType);
484511

485512
const referenceType: Tsoa.ReferenceType = {
486513
additionalProperties,
487514
dataType: 'refObject',
488-
description: this.getNodeDescription(modelType),
515+
description,
489516
properties: inheritedProperties,
490517
refName: this.getRefTypeName(name),
491518
...(example && { example }),

tests/fixtures/controllers/getController.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Readable } from 'stream';
22
import { Controller, Example, Get, OperationId, Query, Request, Route, SuccessResponse, Tags } from '../../../src';
33
import '../duplicateTestModel';
4-
import { GenericModel, TestClassModel, TestModel, TestSubModel } from '../testModel';
4+
import { GenericModel, GetterClass, GetterInterface, GetterInterfaceHerited, TestClassModel, TestModel, TestSubModel, SimpleClassWithToJSON } from '../testModel';
55
import { ModelService } from './../services/modelService';
66

77
@Route('GetTest')
@@ -58,6 +58,26 @@ export class GetTestController extends Controller {
5858
return new ModelService().getClassModel();
5959
}
6060

61+
@Get('GetterClass')
62+
public async getGetterClass(): Promise<GetterClass> {
63+
return new GetterClass();
64+
}
65+
66+
@Get('SimpleClassWithToJSON')
67+
public async simpleClassWithToJSON(): Promise<SimpleClassWithToJSON> {
68+
return new SimpleClassWithToJSON('hello, world', true);
69+
}
70+
71+
@Get('GetterInterface')
72+
public async getGetterInterface(): Promise<GetterInterface> {
73+
return {} as GetterInterface;
74+
}
75+
76+
@Get('GetterInterfaceHerited')
77+
public async getGetterInterfaceHerited(): Promise<GetterInterfaceHerited> {
78+
return {} as GetterInterfaceHerited;
79+
}
80+
6181
@Get('Multi')
6282
public async getMultipleModels(): Promise<TestModel[]> {
6383
return [new ModelService().getModel(), new ModelService().getModel(), new ModelService().getModel()];

tests/fixtures/testModel.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,48 @@ export class TestClassModel extends TestClassBaseModel {
647647
) {
648648
super();
649649
}
650+
651+
public myIgnoredMethod() {
652+
return 'ignored';
653+
}
654+
}
655+
656+
type NonFunctionPropertyNames<T> = {
657+
[K in keyof T]: T[K] extends Function ? never : K;
658+
}[keyof T];
659+
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
660+
export class GetterClass {
661+
public a: 'b';
662+
663+
get foo() {
664+
return 'bar';
665+
}
666+
667+
public toJSON(): NonFunctionProperties<GetterClass> & { foo: string } {
668+
return Object.assign({}, this, { foo: this.foo });
669+
}
670+
}
671+
672+
export class SimpleClassWithToJSON {
673+
public a: string;
674+
public b: boolean;
675+
676+
constructor(a: string, b: boolean) {
677+
this.a = a;
678+
this.b = b;
679+
}
680+
681+
public toJSON(): { a: string } {
682+
return { a: this.a };
683+
}
684+
}
685+
686+
export interface GetterInterface {
687+
toJSON(): { foo: string };
688+
}
689+
690+
export interface GetterInterfaceHerited extends GetterInterface {
691+
foo: number;
650692
}
651693

652694
export interface GenericModel<T = string> {

tests/integration/dynamic-controllers-express-server.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ describe('Express Server', () => {
4747
});
4848
});
4949

50+
it('respects toJSON for class serialization', () => {
51+
return verifyGetRequest(basePath + '/GetTest/SimpleClassWithToJSON', (err, res) => {
52+
const getterClass = res.body;
53+
expect(getterClass).to.haveOwnProperty('a');
54+
expect(getterClass.a).to.equal('hello, world');
55+
expect(getterClass).to.not.haveOwnProperty('b');
56+
});
57+
});
58+
5059
it('can handle get request with collection return value', () => {
5160
return verifyGetRequest(basePath + '/GetTest/Multi', (err, res) => {
5261
const models = res.body as TestModel[];

tests/integration/express-server.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ describe('Express Server', () => {
4747
});
4848
});
4949

50+
it('respects toJSON for class serialization', () => {
51+
return verifyGetRequest(basePath + '/GetTest/SimpleClassWithToJSON', (err, res) => {
52+
const getterClass = res.body;
53+
expect(getterClass).to.haveOwnProperty('a');
54+
expect(getterClass.a).to.equal('hello, world');
55+
expect(getterClass).to.not.haveOwnProperty('b');
56+
});
57+
});
58+
5059
it('can handle get request with collection return value', () => {
5160
return verifyGetRequest(basePath + '/GetTest/Multi', (err, res) => {
5261
const models = res.body as TestModel[];

tests/integration/hapi-server.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ describe('Hapi Server', () => {
3535
});
3636
});
3737

38+
it('respects toJSON for class serialization', () => {
39+
return verifyGetRequest(basePath + '/GetTest/SimpleClassWithToJSON', (err, res) => {
40+
const getterClass = res.body;
41+
expect(getterClass).to.haveOwnProperty('a');
42+
expect(getterClass.a).to.equal('hello, world');
43+
expect(getterClass).to.not.haveOwnProperty('b');
44+
});
45+
});
46+
3847
it('can handle get request with collection return value', () => {
3948
return verifyGetRequest(basePath + '/GetTest/Multi', (err, res) => {
4049
const models = res.body as TestModel[];

tests/integration/koa-server.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ describe('Koa Server', () => {
3636
});
3737
});
3838

39+
it('respects toJSON for class serialization', () => {
40+
return verifyGetRequest(basePath + '/GetTest/SimpleClassWithToJSON', (err, res) => {
41+
const getterClass = res.body;
42+
expect(getterClass).to.haveOwnProperty('a');
43+
expect(getterClass.a).to.equal('hello, world');
44+
expect(getterClass).to.not.haveOwnProperty('b');
45+
});
46+
});
47+
3948
it('can handle get request with collection return value', () => {
4049
return verifyGetRequest(basePath + '/GetTest/Multi', (err, res) => {
4150
const models = res.body as TestModel[];

tests/unit/swagger/definitionsGeneration/definitions.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,7 @@ describe('Definition generation', () => {
787787
'publicConstructorVar',
788788
'readonlyConstructorArgument',
789789
'optionalPublicConstructorVar',
790+
'myIgnoredMethod',
790791
'defaultValue1',
791792
],
792793
description: 'Exclude from T those types that are assignable to U',

tests/unit/swagger/schemaDetails3.spec.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,98 @@ describe('Definition generation for OpenAPI 3.0.0', () => {
830830
`for property ${propertyName}`,
831831
);
832832

833+
const getterClass = getComponentSchema('GetterClass', currentSpec);
834+
expect(getterClass).to.deep.eq({
835+
allOf: [
836+
{
837+
$ref: '#/components/schemas/NonFunctionProperties_GetterClass_',
838+
},
839+
{
840+
properties: {
841+
foo: {
842+
type: 'string',
843+
description: undefined,
844+
example: undefined,
845+
format: undefined,
846+
default: undefined,
847+
},
848+
},
849+
required: ['foo'],
850+
type: 'object',
851+
},
852+
],
853+
default: undefined,
854+
example: undefined,
855+
format: undefined,
856+
description: undefined,
857+
});
858+
const getterClass2 = getComponentSchema('NonFunctionProperties_GetterClass_', currentSpec);
859+
expect(getterClass2).to.deep.eq({
860+
$ref: '#/components/schemas/Pick_GetterClass.NonFunctionPropertyNames_GetterClass__',
861+
description: undefined,
862+
example: undefined,
863+
format: undefined,
864+
default: undefined,
865+
});
866+
const getterClass3 = getComponentSchema('Pick_GetterClass.NonFunctionPropertyNames_GetterClass__', currentSpec);
867+
expect(getterClass3).to.deep.eq({
868+
default: undefined,
869+
description: 'From T, pick a set of properties whose keys are in the union K',
870+
example: undefined,
871+
format: undefined,
872+
properties: {
873+
a: {
874+
type: 'string',
875+
enum: ['b'],
876+
nullable: false,
877+
description: undefined,
878+
example: undefined,
879+
format: undefined,
880+
default: undefined,
881+
},
882+
},
883+
required: ['a'],
884+
type: 'object',
885+
});
886+
887+
const getterInterface = getComponentSchema('GetterInterface', currentSpec);
888+
expect(getterInterface).to.deep.eq({
889+
properties: {
890+
foo: {
891+
type: 'string',
892+
description: undefined,
893+
example: undefined,
894+
format: undefined,
895+
default: undefined,
896+
},
897+
},
898+
required: ['foo'],
899+
type: 'object',
900+
default: undefined,
901+
example: undefined,
902+
format: undefined,
903+
description: undefined,
904+
});
905+
906+
const getterInterfaceHerited = getComponentSchema('GetterInterfaceHerited', currentSpec);
907+
expect(getterInterfaceHerited).to.deep.eq({
908+
properties: {
909+
foo: {
910+
type: 'string',
911+
description: undefined,
912+
example: undefined,
913+
format: undefined,
914+
default: undefined,
915+
},
916+
},
917+
required: ['foo'],
918+
type: 'object',
919+
default: undefined,
920+
example: undefined,
921+
format: undefined,
922+
description: undefined,
923+
});
924+
833925
const omit = getComponentSchema('Omit_ErrorResponseModel.status_', currentSpec);
834926
expect(omit).to.deep.eq(
835927
{
@@ -951,6 +1043,7 @@ describe('Definition generation for OpenAPI 3.0.0', () => {
9511043
{ type: 'string', enum: ['publicConstructorVar'], nullable: false },
9521044
{ type: 'string', enum: ['readonlyConstructorArgument'], nullable: false },
9531045
{ type: 'string', enum: ['optionalPublicConstructorVar'], nullable: false },
1046+
{ type: 'string', enum: ['myIgnoredMethod'], nullable: false },
9541047
{ type: 'string', enum: ['defaultValue1'], nullable: false },
9551048
],
9561049
description: 'Exclude from T those types that are assignable to U',

0 commit comments

Comments
 (0)