Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 30 additions & 19 deletions packages/cli/src/swagger/specGenerator2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,32 @@ export class SpecGenerator2 extends SpecGenerator {
super(metadata, config);
}

// In Swagger 2.0, exclusiveMinimum/exclusiveMaximum are boolean modifiers on minimum/maximum.
private transformValidatorsForSchema(validators: Tsoa.Validators): Record<string, unknown> {
if (validators.exclusiveMinimum !== undefined && validators.minimum !== undefined) {
throw new Error('Cannot use both @minimum and @exclusiveMinimum in Swagger 2.0. Use one or the other, or target OpenAPI 3.1.');
}
if (validators.exclusiveMaximum !== undefined && validators.maximum !== undefined) {
throw new Error('Cannot use both @maximum and @exclusiveMaximum in Swagger 2.0. Use one or the other, or target OpenAPI 3.1.');
}

const result: Record<string, unknown> = {};
Object.keys(validators)
.filter(shouldIncludeValidatorInSchema)
.forEach(key => {
if (key === 'exclusiveMinimum') {
result['minimum'] = validators.exclusiveMinimum!.value;
result['exclusiveMinimum'] = true;
} else if (key === 'exclusiveMaximum') {
result['maximum'] = validators.exclusiveMaximum!.value;
result['exclusiveMaximum'] = true;
} else {
result[key] = validators[key]!.value;
}
});
return result;
}

public GetSpec() {
let spec: Swagger.Spec2 = {
basePath: normalisePath(this.config.basePath as string, '/', undefined, false),
Expand Down Expand Up @@ -129,14 +155,7 @@ export class SpecGenerator2 extends SpecGenerator {
} else if (referenceType.dataType === 'refAlias') {
const swaggerType = this.getSwaggerType(referenceType.type);
const format = referenceType.format as Swagger.DataFormat;
const validators = Object.keys(referenceType.validators)
.filter(shouldIncludeValidatorInSchema)
.reduce((acc, key) => {
return {
...acc,
[key]: referenceType.validators[key]!.value,
};
}, {});
const validators = this.transformValidatorsForSchema(referenceType.validators);

definitions[referenceType.refName] = {
...(swaggerType as Swagger.Schema2),
Expand Down Expand Up @@ -365,12 +384,7 @@ export class SpecGenerator2 extends SpecGenerator {
return parameter;
}

const validatorObjs: Partial<Record<Tsoa.SchemaValidatorKey, unknown>> = {};
Object.keys(source.validators)
.filter(shouldIncludeValidatorInSchema)
.forEach(key => {
validatorObjs[key] = source.validators[key]!.value;
});
const validatorObjs = this.transformValidatorsForSchema(source.validators);

if (source.in === 'body' && source.type.dataType === 'array') {
parameter.schema = {
Expand Down Expand Up @@ -414,11 +428,8 @@ export class SpecGenerator2 extends SpecGenerator {
if (!swaggerType.$ref) {
swaggerType.default = property.default;

Object.keys(property.validators)
.filter(shouldIncludeValidatorInSchema)
.forEach(key => {
swaggerType = { ...swaggerType, [key]: property.validators[key]!.value };
});
const propertyValidators = this.transformValidatorsForSchema(property.validators);
swaggerType = { ...swaggerType, ...propertyValidators };
}
if (property.deprecated) {
swaggerType['x-deprecated'] = true;
Expand Down
59 changes: 32 additions & 27 deletions packages/cli/src/swagger/specGenerator3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,33 @@ export class SpecGenerator3 extends SpecGenerator {
super(metadata, config);
}

// In OAS 3.0, exclusiveMinimum/exclusiveMaximum are boolean modifiers on minimum/maximum.
// OAS 3.1 overrides this to pass through numeric values directly.
protected transformValidatorsForSchema(validators: Tsoa.Validators): Record<string, unknown> {
if (validators.exclusiveMinimum !== undefined && validators.minimum !== undefined) {
throw new Error('Cannot use both @minimum and @exclusiveMinimum in OpenAPI 3.0. Use one or the other, or target OpenAPI 3.1.');
}
if (validators.exclusiveMaximum !== undefined && validators.maximum !== undefined) {
throw new Error('Cannot use both @maximum and @exclusiveMaximum in OpenAPI 3.0. Use one or the other, or target OpenAPI 3.1.');
}

const result: Record<string, unknown> = {};
Object.keys(validators)
.filter(shouldIncludeValidatorInSchema)
.forEach(key => {
if (key === 'exclusiveMinimum') {
result['minimum'] = validators.exclusiveMinimum!.value;
result['exclusiveMinimum'] = true;
} else if (key === 'exclusiveMaximum') {
result['maximum'] = validators.exclusiveMaximum!.value;
result['exclusiveMaximum'] = true;
} else {
result[key] = validators[key]!.value;
}
});
return result;
}

public GetSpec(): Swagger.Spec3 {
let spec: Swagger.Spec30 = {
openapi: '3.0.0',
Expand Down Expand Up @@ -214,14 +241,7 @@ export class SpecGenerator3 extends SpecGenerator {
} else if (referenceType.dataType === 'refAlias') {
const swaggerType = this.getSwaggerType(referenceType.type);
const format = referenceType.format as Swagger.DataFormat;
const validators = Object.keys(referenceType.validators)
.filter(shouldIncludeValidatorInSchema)
.reduce((acc, key) => {
return {
...acc,
[key]: referenceType.validators[key]!.value,
};
}, {});
const validators = this.transformValidatorsForSchema(referenceType.validators);

schema[referenceType.refName] = {
...(swaggerType as Swagger.Schema3),
Expand Down Expand Up @@ -448,14 +468,7 @@ export class SpecGenerator3 extends SpecGenerator {
}

protected buildMediaType(controllerName: string, method: Tsoa.Method, parameter: Tsoa.Parameter): Swagger.MediaType {
const validators = Object.keys(parameter.validators)
.filter(shouldIncludeValidatorInSchema)
.reduce((acc, key) => {
return {
...acc,
[key]: parameter.validators[key]!.value,
};
}, {});
const validators = this.transformValidatorsForSchema(parameter.validators);

const mediaType: Swagger.MediaType = {
schema: {
Expand Down Expand Up @@ -516,12 +529,7 @@ export class SpecGenerator3 extends SpecGenerator {
return Object.assign(parameter, this.buildExamples(source));
}

const validatorObjs: { [key in Tsoa.SchemaValidatorKey]?: unknown } = {};
Object.keys(source.validators)
.filter(shouldIncludeValidatorInSchema)
.forEach(key => {
validatorObjs[key] = source.validators[key]!.value;
});
const validatorObjs = this.transformValidatorsForSchema(source.validators);

if (source.type.dataType === 'any') {
parameter.schema.type = 'string';
Expand Down Expand Up @@ -578,11 +586,8 @@ export class SpecGenerator3 extends SpecGenerator {
if (!swaggerType.$ref) {
swaggerType.default = property.default;

Object.keys(property.validators)
.filter(shouldIncludeValidatorInSchema)
.forEach(key => {
swaggerType = { ...swaggerType, [key]: property.validators[key]!.value };
});
const propertyValidators = this.transformValidatorsForSchema(property.validators);
swaggerType = { ...swaggerType, ...propertyValidators };
}
if (property.deprecated) {
swaggerType.deprecated = true;
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/swagger/specGenerator31.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { merge as deepMerge } from 'ts-deepmerge';

import { ExtendedSpecConfig } from '../cli';
import { UnspecifiedObject } from '../utils/unspecifiedObject';
import { shouldIncludeValidatorInSchema } from '../utils/validatorUtils';
import { SpecGenerator3 } from './specGenerator3';

/**
Expand Down Expand Up @@ -50,6 +51,17 @@ export class SpecGenerator31 extends SpecGenerator3 {
return spec;
}

// In OAS 3.1, exclusiveMinimum/exclusiveMaximum are standalone numbers.
protected override transformValidatorsForSchema(validators: Tsoa.Validators): Record<string, unknown> {
const result: Record<string, unknown> = {};
Object.keys(validators)
.filter(shouldIncludeValidatorInSchema)
.forEach(key => {
result[key] = validators[key]!.value;
});
return result;
}

/**
* Override to add tuple type support (OpenAPI 3.1 feature via prefixItems)
*/
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/utils/validatorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export function getParameterValidators(parameter: ts.ParameterDeclaration, param
break;
case 'minimum':
case 'maximum':
case 'exclusiveMinimum':
case 'exclusiveMaximum':
case 'minItems':
case 'maxItems':
case 'minLength':
Expand Down Expand Up @@ -152,6 +154,8 @@ export function getPropertyValidators(property: ts.Node): Tsoa.Validators | unde
break;
case 'minimum':
case 'maximum':
case 'exclusiveMinimum':
case 'exclusiveMaximum':
case 'minItems':
case 'maxItems':
case 'minLength':
Expand Down Expand Up @@ -228,6 +232,8 @@ function getParameterTagSupport() {
'pattern',
'minimum',
'maximum',
'exclusiveMinimum',
'exclusiveMaximum',
'minDate',
'maxDate',
'title',
Expand Down
40 changes: 40 additions & 0 deletions packages/runtime/src/routeGeneration/templateHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,24 @@ export class ValidationService {
return;
}
}
if (validators.exclusiveMinimum && validators.exclusiveMinimum.value !== undefined) {
if (validators.exclusiveMinimum.value >= numberValue) {
fieldErrors[parent + name] = {
message: validators.exclusiveMinimum.errorMsg || `exclusiveMin ${validators.exclusiveMinimum.value}`,
value,
};
return;
}
}
if (validators.exclusiveMaximum && validators.exclusiveMaximum.value !== undefined) {
if (validators.exclusiveMaximum.value <= numberValue) {
fieldErrors[parent + name] = {
message: validators.exclusiveMaximum.errorMsg || `exclusiveMax ${validators.exclusiveMaximum.value}`,
value,
};
return;
}
}
return numberValue;
}

Expand Down Expand Up @@ -266,6 +284,24 @@ export class ValidationService {
return;
}
}
if (validators.exclusiveMinimum && validators.exclusiveMinimum.value !== undefined) {
if (validators.exclusiveMinimum.value >= numberValue) {
fieldErrors[parent + name] = {
message: validators.exclusiveMinimum.errorMsg || `exclusiveMin ${validators.exclusiveMinimum.value}`,
value,
};
return;
}
}
if (validators.exclusiveMaximum && validators.exclusiveMaximum.value !== undefined) {
if (validators.exclusiveMaximum.value <= numberValue) {
fieldErrors[parent + name] = {
message: validators.exclusiveMaximum.errorMsg || `exclusiveMax ${validators.exclusiveMaximum.value}`,
value,
};
return;
}
}
return numberValue;
}

Expand Down Expand Up @@ -940,13 +976,17 @@ export interface IntegerValidator {
isLong?: { errorMsg?: string };
minimum?: { value: number; errorMsg?: string };
maximum?: { value: number; errorMsg?: string };
exclusiveMinimum?: { value: number; errorMsg?: string };
exclusiveMaximum?: { value: number; errorMsg?: string };
}

export interface FloatValidator {
isFloat?: { errorMsg?: string };
isDouble?: { errorMsg?: string };
minimum?: { value: number; errorMsg?: string };
maximum?: { value: number; errorMsg?: string };
exclusiveMinimum?: { value: number; errorMsg?: string };
exclusiveMaximum?: { value: number; errorMsg?: string };
}

export interface DateValidator {
Expand Down
10 changes: 7 additions & 3 deletions packages/runtime/src/swagger/swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,9 +339,7 @@ export namespace Swagger {
default?: string | boolean | number | unknown;
multipleOf?: number;
maximum?: number;
exclusiveMaximum?: number;
minimum?: number;
exclusiveMinimum?: number;
maxLength?: number;
minLength?: number;
pattern?: string;
Expand All @@ -368,8 +366,10 @@ export namespace Swagger {
items?: BaseSchema;
}

export interface Schema31 extends Omit<Schema3, 'items' | 'properties' | 'additionalProperties' | 'discriminator' | 'anyOf' | 'allOf'> {
export interface Schema31 extends Omit<Schema3, 'items' | 'properties' | 'additionalProperties' | 'discriminator' | 'anyOf' | 'allOf' | 'exclusiveMinimum' | 'exclusiveMaximum'> {
examples?: unknown[];
exclusiveMinimum?: number;
exclusiveMaximum?: number;

properties?: { [key: string]: Schema31 };
additionalProperties?: boolean | Schema31;
Expand Down Expand Up @@ -397,13 +397,17 @@ export namespace Swagger {
allOf?: BaseSchema[];
deprecated?: boolean;
properties?: { [propertyName: string]: Schema3 };
exclusiveMinimum?: boolean;
exclusiveMaximum?: boolean;
}

export interface Schema2 extends BaseSchema {
type?: DataType;
properties?: { [propertyName: string]: Schema2 };
['x-nullable']?: boolean;
['x-deprecated']?: boolean;
exclusiveMinimum?: boolean;
exclusiveMaximum?: boolean;
}

export interface Header {
Expand Down
16 changes: 16 additions & 0 deletions tests/fixtures/testModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,14 @@ export class ValidateModel {
* @minimum 5
*/
public numberMin5!: number;
/**
* @exclusiveMinimum 5
*/
public numberExclusiveMin5!: number;
/**
* @exclusiveMaximum 10
*/
public numberExclusiveMax10!: number;
/**
* @maxLength 10
*/
Expand Down Expand Up @@ -990,6 +998,14 @@ export class ValidateModel {
* @minimum 5
*/
numberMin5: number;
/**
* @exclusiveMinimum 5
*/
numberExclusiveMin5: number;
/**
* @exclusiveMaximum 10
*/
numberExclusiveMax10: number;
/**
* @maxLength 10
*/
Expand Down
16 changes: 16 additions & 0 deletions tests/fixtures/testModel31.ts
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,14 @@ export class ValidateModel {
* @minimum 5
*/
public numberMin5!: number;
/**
* @exclusiveMinimum 5
*/
public numberExclusiveMin5!: number;
/**
* @exclusiveMaximum 10
*/
public numberExclusiveMax10!: number;
/**
* @maxLength 10
*/
Expand Down Expand Up @@ -982,6 +990,14 @@ export class ValidateModel {
* @minimum 5
*/
numberMin5: number;
/**
* @exclusiveMinimum 5
*/
numberExclusiveMin5: number;
/**
* @exclusiveMaximum 10
*/
numberExclusiveMax10: number;
/**
* @maxLength 10
*/
Expand Down
Loading
Loading