Skip to content

Commit f0f9aa7

Browse files
authored
Merge pull request #1810 from orgoro/feature/title-annotation
title: add from annotation
2 parents 6595d72 + a8b0161 commit f0f9aa7

13 files changed

Lines changed: 220 additions & 3 deletions

File tree

packages/cli/src/metadataGeneration/transformer/enumTransformer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export class EnumTransformer extends Transformer {
2828
const enumVarnames = first.enumVarnames ? (second.enumVarnames ? [...first.enumVarnames, ...second.enumVarnames] : first.enumVarnames) : second.enumVarnames;
2929

3030
const example = first.example || second.example;
31+
const title = first.title || second.title;
3132

3233
return {
3334
dataType: 'refEnum',
@@ -37,6 +38,7 @@ export class EnumTransformer extends Transformer {
3738
refName: first.refName,
3839
deprecated,
3940
example,
41+
...(title && { title }),
4042
};
4143
}
4244

@@ -57,6 +59,7 @@ export class EnumTransformer extends Transformer {
5759
};
5860
const enums = declaration.members.map(e => resolver.current.typeChecker.getConstantValue(e)).filter(isNotUndefined);
5961
const enumVarnames = declaration.members.map(e => e.name.getText()).filter(isNotUndefined);
62+
const title = resolver.getNodeTitle(declaration);
6063

6164
return {
6265
dataType: 'refEnum',
@@ -66,6 +69,7 @@ export class EnumTransformer extends Transformer {
6669
enumVarnames,
6770
refName: enumName,
6871
deprecated: isExistJSDocTag(declaration, tag => tag.tagName.text === 'deprecated'),
72+
...(title && { title }),
6973
};
7074
}
7175

packages/cli/src/metadataGeneration/transformer/propertyTransformer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export class PropertyTransformer extends Transformer {
6868
type: new TypeResolver(propertySignature.type, resolver.current, propertySignature.type.parent, resolver.context).resolve(),
6969
validators: getPropertyValidators(propertySignature) || {},
7070
deprecated: isExistJSDocTag(propertySignature, tag => tag.tagName.text === 'deprecated'),
71+
title: resolver.getNodeTitle(propertySignature),
7172
extensions: resolver.getNodeExtension(propertySignature),
7273
};
7374
return property;
@@ -107,6 +108,7 @@ export class PropertyTransformer extends Transformer {
107108
validators: getPropertyValidators(propertyDeclaration) || {},
108109
// class properties and constructor parameters may be deprecated either via jsdoc annotation or decorator
109110
deprecated: isExistJSDocTag(propertyDeclaration, tag => tag.tagName.text === 'deprecated') || isDecorator(propertyDeclaration, identifier => identifier.text === 'Deprecated'),
111+
title: resolver.getNodeTitle(propertyDeclaration),
110112
extensions: resolver.getNodeExtension(propertyDeclaration),
111113
};
112114
return property;

packages/cli/src/metadataGeneration/transformer/referenceTransformer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export class ReferenceTransformer extends Transformer {
5959
: first.additionalProperties
6060
: second.additionalProperties;
6161

62+
const title = first.title || second.title;
6263
const result: Tsoa.RefObjectType = {
6364
dataType: 'refObject',
6465
description,
@@ -67,6 +68,7 @@ export class ReferenceTransformer extends Transformer {
6768
refName: first.refName,
6869
deprecated,
6970
example,
71+
...(title && { title }),
7072
};
7173

7274
return result;
@@ -75,6 +77,7 @@ export class ReferenceTransformer extends Transformer {
7577
public transform(declaration: TypeAliasDeclaration, refTypeName: string, resolver: TypeResolver, referencer?: Type): Tsoa.ReferenceType {
7678
const example = resolver.getNodeExample(declaration);
7779

80+
const title = resolver.getNodeTitle(declaration);
7881
const referenceType: Tsoa.ReferenceType = {
7982
dataType: 'refAlias',
8083
default: TypeResolver.getDefault(declaration),
@@ -84,6 +87,7 @@ export class ReferenceTransformer extends Transformer {
8487
type: new TypeResolver(declaration.type, resolver.current, declaration, resolver.context, resolver.referencer || referencer).resolve(),
8588
validators: getPropertyValidators(declaration) || {},
8689
...(example && { example }),
90+
...(title && { title }),
8791
};
8892
return referenceType;
8993
}

packages/cli/src/metadataGeneration/typeResolver.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ export class TypeResolver {
148148
type,
149149
validators: getPropertyValidators(propertySignature) || {},
150150
deprecated: isExistJSDocTag(propertySignature, tag => tag.tagName.text === 'deprecated'),
151+
title: this.getNodeTitle(propertySignature),
151152
extensions: this.getNodeExtension(propertySignature),
152153
};
153154

@@ -905,6 +906,7 @@ export class TypeResolver {
905906
const example = this.getNodeExample(modelType);
906907
const description = this.getNodeDescription(modelType);
907908
const deprecated = isExistJSDocTag(modelType, tag => tag.tagName.text === 'deprecated') || isDecorator(modelType, identifier => identifier.text === 'Deprecated');
909+
const title = this.getNodeTitle(modelType);
908910

909911
// Handle toJSON methods
910912
throwUnless(modelType.name, new GenerateMetadataError("Can't get Symbol from anonymous class", modelType));
@@ -927,6 +929,7 @@ export class TypeResolver {
927929
validators: {},
928930
deprecated,
929931
...(example && { example }),
932+
...(title && { title }),
930933
};
931934
return referenceType;
932935
}
@@ -943,6 +946,7 @@ export class TypeResolver {
943946
refName: refTypeName,
944947
deprecated,
945948
...(example && { example }),
949+
...(title && { title }),
946950
};
947951

948952
referenceType.properties = referenceType.properties.concat(properties);
@@ -1194,6 +1198,10 @@ export class TypeResolver {
11941198
return getJSDocComment(node, 'format');
11951199
}
11961200

1201+
public getNodeTitle(node: ts.Node) {
1202+
return getJSDocComment(node, 'title');
1203+
}
1204+
11971205
public getPropertyName(prop: ts.PropertySignature | ts.PropertyDeclaration | ts.ParameterDeclaration): string {
11981206
if (ts.isComputedPropertyName(prop.name) && ts.isPropertyAccessExpression(prop.name.expression)) {
11991207
const initializerValue = getInitializerValue(prop.name.expression, this.current.typeChecker);

packages/cli/src/swagger/specGenerator2.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ export class SpecGenerator2 extends SpecGenerator {
107107
if (referenceType.example) {
108108
definitions[referenceType.refName].example = referenceType.example;
109109
}
110+
111+
if (referenceType.title) {
112+
definitions[referenceType.refName].title = referenceType.title;
113+
}
110114
} else if (referenceType.dataType === 'refEnum') {
111115
definitions[referenceType.refName] = {
112116
description: referenceType.description,
@@ -119,6 +123,9 @@ export class SpecGenerator2 extends SpecGenerator {
119123
if (referenceType.example) {
120124
definitions[referenceType.refName].example = referenceType.example;
121125
}
126+
if (referenceType.title) {
127+
definitions[referenceType.refName].title = referenceType.title;
128+
}
122129
} else if (referenceType.dataType === 'refAlias') {
123130
const swaggerType = this.getSwaggerType(referenceType.type);
124131
const format = referenceType.format as Swagger.DataFormat;
@@ -137,6 +144,7 @@ export class SpecGenerator2 extends SpecGenerator {
137144
example: referenceType.example,
138145
format: format || swaggerType.format,
139146
description: referenceType.description,
147+
...(referenceType.title && { title: referenceType.title }),
140148
...validators,
141149
};
142150
} else {
@@ -416,6 +424,10 @@ export class SpecGenerator2 extends SpecGenerator {
416424
swaggerType['x-deprecated'] = true;
417425
}
418426

427+
if (property.title) {
428+
swaggerType.title = property.title;
429+
}
430+
419431
if (property.extensions) {
420432
property.extensions.forEach(property => {
421433
swaggerType[property.key] = property.value;

packages/cli/src/swagger/specGenerator3.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ export class SpecGenerator3 extends SpecGenerator {
174174
if (referenceType.example) {
175175
schema[referenceType.refName].example = referenceType.example;
176176
}
177+
178+
if (referenceType.title) {
179+
schema[referenceType.refName].title = referenceType.title;
180+
}
177181
} else if (referenceType.dataType === 'refEnum') {
178182
const enumTypes = this.determineTypesUsedInEnum(referenceType.enums);
179183

@@ -204,6 +208,9 @@ export class SpecGenerator3 extends SpecGenerator {
204208
if (referenceType.example) {
205209
schema[referenceType.refName].example = referenceType.example;
206210
}
211+
if (referenceType.title) {
212+
schema[referenceType.refName].title = referenceType.title;
213+
}
207214
} else if (referenceType.dataType === 'refAlias') {
208215
const swaggerType = this.getSwaggerType(referenceType.type);
209216
const format = referenceType.format as Swagger.DataFormat;
@@ -222,6 +229,7 @@ export class SpecGenerator3 extends SpecGenerator {
222229
example: referenceType.example,
223230
format: format || swaggerType.format,
224231
description: referenceType.description,
232+
...(referenceType.title && { title: referenceType.title }),
225233
...validators,
226234
};
227235
} else {
@@ -580,6 +588,10 @@ export class SpecGenerator3 extends SpecGenerator {
580588
swaggerType.deprecated = true;
581589
}
582590

591+
if (property.title) {
592+
swaggerType.title = property.title;
593+
}
594+
583595
if (property.extensions) {
584596
property.extensions.forEach(property => {
585597
swaggerType[property.key] = property.value;

packages/runtime/src/metadataGeneration/tsoa.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export namespace Tsoa {
4949
validators: Validators;
5050
deprecated: boolean;
5151
exampleLabels?: Array<string | undefined>;
52+
title?: string;
5253
$ref?: Swagger.BaseSchema;
5354
}
5455

@@ -99,6 +100,7 @@ export namespace Tsoa {
99100
validators: Validators;
100101
deprecated: boolean;
101102
extensions?: Extension[];
103+
title?: string;
102104
}
103105

104106
export type TypeStringLiteral =
@@ -272,6 +274,7 @@ export namespace Tsoa {
272274
refName: string;
273275
example?: unknown;
274276
deprecated: boolean;
277+
title?: string;
275278
}
276279

277280
export interface UnionType extends TypeBase {

tests/fixtures/testModel.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ export interface TestModel extends Model {
181181

182182
// prettier-ignore
183183
stringAndBoolArray?: Array<(string | boolean)>;
184+
testModelWithAnnotations?: TestModelWithAnnotations;
185+
enumWithTitle?: EnumWithTitle;
184186

185187
/**
186188
* @example {
@@ -1295,3 +1297,21 @@ type OrderDirection = 'asc' | 'desc';
12951297
type OrderOptions<E> = `${keyof E & string}:${OrderDirection}`;
12961298

12971299
type TemplateLiteralString = OrderOptions<ParameterTestModel>;
1300+
1301+
/**
1302+
* @title Title annotation for model
1303+
*/
1304+
interface TestModelWithAnnotations {
1305+
/**
1306+
* @title Title annotation for property
1307+
*/
1308+
param: string;
1309+
}
1310+
1311+
/**
1312+
* @title Title annotation for enum
1313+
*/
1314+
export enum EnumWithTitle {
1315+
Value1 = 'value1',
1316+
Value2 = 'value2',
1317+
}

tests/unit/metadataGeneration/transformer/enumTransformer.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,57 @@ describe('EnumTransformer - Null Safety', () => {
207207
example: 'example1', // First example should be used
208208
});
209209
});
210+
211+
it('should merge title property correctly', () => {
212+
const first: Tsoa.RefEnumType = {
213+
dataType: 'refEnum',
214+
refName: 'TestEnum',
215+
enums: ['value1'],
216+
enumVarnames: ['VALUE1'],
217+
description: 'First enum',
218+
deprecated: false,
219+
title: 'First Title',
220+
};
221+
222+
const second: Tsoa.RefEnumType = {
223+
dataType: 'refEnum',
224+
refName: 'TestEnum',
225+
enums: ['value2'],
226+
enumVarnames: ['VALUE2'],
227+
description: 'Second enum',
228+
deprecated: false,
229+
title: 'Second Title',
230+
};
231+
232+
const result = EnumTransformer.merge(first, second);
233+
234+
expect(result.title).to.equal('First Title'); // First title should be used
235+
});
236+
237+
it('should use second title if first is undefined', () => {
238+
const first: Tsoa.RefEnumType = {
239+
dataType: 'refEnum',
240+
refName: 'TestEnum',
241+
enums: ['value1'],
242+
enumVarnames: ['VALUE1'],
243+
description: 'First enum',
244+
deprecated: false,
245+
};
246+
247+
const second: Tsoa.RefEnumType = {
248+
dataType: 'refEnum',
249+
refName: 'TestEnum',
250+
enums: ['value2'],
251+
enumVarnames: ['VALUE2'],
252+
description: 'Second enum',
253+
deprecated: false,
254+
title: 'Second Title',
255+
};
256+
257+
const result = EnumTransformer.merge(first, second);
258+
259+
expect(result.title).to.equal('Second Title');
260+
});
210261
});
211262

212263
describe('mergeMany method', () => {

tests/unit/metadataGeneration/transformer/referenceTransformer.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,43 @@ describe('ReferenceTransformer - Empty Array Handling', () => {
255255
});
256256

257257
describe('mergeManyRefObj method', () => {
258+
it('should merge title field from refObjects', () => {
259+
const refObjects: Tsoa.RefObjectType[] = [
260+
{
261+
dataType: 'refObject',
262+
refName: 'TestObject',
263+
properties: [createProperty('id', { dataType: 'string' })],
264+
additionalProperties: false as any,
265+
description: 'First object',
266+
deprecated: false,
267+
title: 'FirstTitle',
268+
},
269+
{
270+
dataType: 'refObject',
271+
refName: 'TestObject',
272+
properties: [createProperty('name', { dataType: 'string' })],
273+
additionalProperties: false as any,
274+
description: 'Second object',
275+
deprecated: false,
276+
title: 'SecondTitle',
277+
},
278+
];
279+
280+
const result = ReferenceTransformer.mergeManyRefObj(refObjects);
281+
282+
expect(result.title).to.equal('FirstTitle');
283+
expect(result).to.include({
284+
dataType: 'refObject',
285+
refName: 'TestObject',
286+
description: 'First object\nSecond object',
287+
deprecated: false,
288+
example: undefined,
289+
});
290+
expect(result.properties).to.deep.include.members([
291+
{ name: 'id', type: { dataType: 'string' }, required: true },
292+
{ name: 'name', type: { dataType: 'string' }, required: true },
293+
]);
294+
});
258295
it('should handle single refObject', () => {
259296
const refObject: Tsoa.RefObjectType = {
260297
dataType: 'refObject',

0 commit comments

Comments
 (0)