Skip to content
Merged
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
60 changes: 60 additions & 0 deletions packages/openapi-generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -497,3 +497,63 @@ const Schema = t.type({
fieldWithFormattedDescription: t.string,
});
```

#### 6.2.4 Enum Documentation

When using `t.keyof` to define enums, you can add descriptions and deprecation notices
to individual enum values. These will be output as `x-enumDescriptions` and
`x-enumsDeprecated` in the OpenAPI specification.

- **`@description`** - Adds a description for a specific enum value. All enum value
descriptions are collected into an `x-enumDescriptions` object in the OpenAPI spec.
- **`@deprecated`** - Marks specific enum values as deprecated. All deprecated enum
values are collected into an `x-enumsDeprecated` array in the OpenAPI spec.

```typescript
import * as t from 'io-ts';

/**
* Transaction status values
*/
export const TransactionStatus = t.keyof(
{
/**
* @description Transaction is waiting for approval from authorized users
*/
pendingApproval: 1,
/**
* @description Transaction was canceled by the user
* @deprecated
*/
canceled: 1,
/**
* @description Transaction was rejected by approvers
* @deprecated
*/
rejected: 1,
/**
* @description Transaction has been successfully completed
*/
completed: 1,
},
'TransactionStatus',
);
```

This will generate the following OpenAPI schema:

```json
{
"TransactionStatus": {
"type": "string",
"enum": ["pendingApproval", "canceled", "rejected", "completed"],
"x-enumDescriptions": {
"pendingApproval": "Transaction is waiting for approval from authorized users",
"canceled": "Transaction was canceled by the user",
"rejected": "Transaction was rejected by approvers",
"completed": "Transaction has been successfully completed"
},
"x-enumsDeprecated": ["canceled", "rejected"]
}
}
```
1 change: 1 addition & 0 deletions packages/openapi-generator/src/ir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type Primitive = {
type: 'string' | 'number' | 'integer' | 'boolean' | 'null';
enum?: (string | number | boolean | null | PseudoBigInt)[];
enumDescriptions?: Record<string, string>;
enumsDeprecated?: string[];
};

export function isPrimitive(schema: Schema): schema is Primitive {
Expand Down
9 changes: 7 additions & 2 deletions packages/openapi-generator/src/knownImports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export const KNOWN_IMPORTS: KnownImports = {

const enumValues = Object.keys(arg.properties);
const enumDescriptions: Record<string, string> = {};
const enumsDeprecated: string[] = [];
let hasDescriptions = false;

for (const prop of enumValues) {
Expand All @@ -139,14 +140,18 @@ export const KNOWN_IMPORTS: KnownImports = {
enumDescriptions[prop] = jsdoc.tags.description;
hasDescriptions = true;
}
if (jsdoc.tags && 'deprecated' in jsdoc.tags) {
enumsDeprecated.push(prop);
}
}
}

if (hasDescriptions) {
if (hasDescriptions || enumsDeprecated.length > 0) {
return E.right({
type: 'string',
enum: enumValues,
enumDescriptions,
...(hasDescriptions ? { enumDescriptions } : {}),
...(enumsDeprecated.length > 0 ? { enumsDeprecated } : {}),
});
} else {
const schemas: Schema[] = enumValues.map((prop) => {
Expand Down
8 changes: 8 additions & 0 deletions packages/openapi-generator/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export function schemaToOpenAPI(
result['x-enumDescriptions'] = schema.enumDescriptions;
}

if (schema.enum && schema.enumsDeprecated) {
result['x-enumsDeprecated'] = schema.enumsDeprecated;
}

return result;
}
case 'integer': {
Expand All @@ -44,6 +48,10 @@ export function schemaToOpenAPI(
result['x-enumDescriptions'] = schema.enumDescriptions;
}

if (schema.enum && schema.enumsDeprecated) {
result['x-enumsDeprecated'] = schema.enumsDeprecated;
}

return result;
}
case 'null':
Expand Down
6 changes: 5 additions & 1 deletion packages/openapi-generator/src/optimize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,11 @@ export function simplifyUnion(schema: Schema, optimize: OptimizeFn): Schema {
const remainder: Schema[] = [];
innerSchemas.forEach((innerSchema) => {
if (isPrimitive(innerSchema) && innerSchema.enum !== undefined) {
if (innerSchema.comment || innerSchema.enumDescriptions) {
if (
innerSchema.comment ||
innerSchema.enumDescriptions ||
innerSchema.enumsDeprecated
) {
remainder.push(innerSchema);
} else {
innerSchema.enum.forEach((value) => {
Expand Down
146 changes: 146 additions & 0 deletions packages/openapi-generator/test/openapi/comments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1813,3 +1813,149 @@ testCase(
},
},
);

const ROUTE_WITH_ENUM_DEPRECATED = `
import * as t from 'io-ts';
import * as h from '@api-ts/io-ts-http';

/**
* Enum with @deprecated tags - should generate x-enumsDeprecated
*/
export const StatusWithDeprecated = t.keyof(
{
/**
* @description Transaction is waiting for approval from authorized users
*/
pendingApproval: 1,
/**
* @description Transaction was canceled by the user
* @deprecated
*/
canceled: 1,
/**
* @description Transaction was rejected by approvers
* @deprecated
*/
rejected: 1,
},
'StatusWithDeprecated',
);

/**
* Enum with only @deprecated tags - should generate x-enumsDeprecated
*/
export const StatusOnlyDeprecated = t.keyof(
{
/** @deprecated */
old: 1,
current: 1,
/** @deprecated */
legacy: 1,
},
'StatusOnlyDeprecated',
);

/**
* Route to test enum deprecated scenarios
*
* @operationId api.v1.enumDeprecatedScenarios
* @tag Test Routes
*/
export const route = h.httpRoute({
path: '/enum-deprecated',
method: 'GET',
request: h.httpRequest({
query: {
withDeprecated: StatusWithDeprecated,
onlyDeprecated: StatusOnlyDeprecated,
},
}),
response: {
200: {
result: t.string
}
},
});
`;

testCase(
'enum deprecated scenarios - @deprecated tags with and without @description',
ROUTE_WITH_ENUM_DEPRECATED,
{
openapi: '3.0.3',
info: {
title: 'Test',
version: '1.0.0',
},
paths: {
'/enum-deprecated': {
get: {
summary: 'Route to test enum deprecated scenarios',
operationId: 'api.v1.enumDeprecatedScenarios',
tags: ['Test Routes'],
parameters: [
{
name: 'withDeprecated',
in: 'query',
required: true,
schema: {
$ref: '#/components/schemas/StatusWithDeprecated',
},
},
{
name: 'onlyDeprecated',
in: 'query',
required: true,
schema: {
$ref: '#/components/schemas/StatusOnlyDeprecated',
},
},
],
responses: {
200: {
description: 'OK',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
result: {
type: 'string',
},
},
required: ['result'],
},
},
},
},
},
},
},
},
components: {
schemas: {
StatusWithDeprecated: {
title: 'StatusWithDeprecated',
description: 'Enum with @deprecated tags - should generate x-enumsDeprecated',
type: 'string',
enum: ['pendingApproval', 'canceled', 'rejected'],
'x-enumDescriptions': {
pendingApproval:
'Transaction is waiting for approval from authorized users',
canceled: 'Transaction was canceled by the user',
rejected: 'Transaction was rejected by approvers',
},
'x-enumsDeprecated': ['canceled', 'rejected'],
},
StatusOnlyDeprecated: {
title: 'StatusOnlyDeprecated',
description:
'Enum with only @deprecated tags - should generate x-enumsDeprecated',
Comment on lines +1952 to +1953
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Love the comments here, that help me follow through test cases that are too long to fit on one screen! Great idea

type: 'string',
enum: ['old', 'current', 'legacy'],
'x-enumsDeprecated': ['old', 'legacy'],
},
},
},
},
);