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
14 changes: 10 additions & 4 deletions packages/openapi-generator/src/knownImports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,16 @@ export const KNOWN_IMPORTS: KnownImports = {
if (arg.type !== 'object') {
return errorLeft(`Unimplemented keyof type ${arg.type}`);
}
const schemas: Schema[] = Object.keys(arg.properties).map((prop) => ({
type: 'string',
enum: [prop],
}));
const schemas: Schema[] = Object.keys(arg.properties).map((prop) => {
const propertySchema = arg.properties[prop];
return {
type: 'string',
enum: [prop],
// Preserve the comment from the original property
...(propertySchema?.comment ? { comment: propertySchema.comment } : {}),
};
});

return E.right({
type: 'union',
schemas,
Expand Down
25 changes: 23 additions & 2 deletions packages/openapi-generator/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,24 @@ function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObjec
// Array types not allowed here
const schema = schemaToOpenAPI(p.schema);

let schemaDescription =
schema && 'description' in schema ? schema.description : undefined;

if (
!schemaDescription &&
!p.schema?.comment?.description &&
schema &&
!('$ref' in schema) &&
schema.type === 'array' &&
'items' in schema &&
schema.items &&
typeof schema.items === 'object' &&
'description' in schema.items
) {
schemaDescription = schema.items.description;
delete schema.items.description; // Only delete when we're using it as parameter description
}

if (schema && 'description' in schema) {
delete schema.description;
}
Expand All @@ -376,10 +394,13 @@ function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObjec
delete schema['x-internal'];
}

const parameterDescription =
p.schema?.comment?.description ?? schemaDescription;

return {
name: p.name,
...(p.schema?.comment?.description !== undefined
? { description: p.schema.comment.description }
...(parameterDescription !== undefined
? { description: parameterDescription }
: {}),
in: p.type,
...(isPrivate ? { 'x-internal': true } : {}),
Expand Down
11 changes: 8 additions & 3 deletions packages/openapi-generator/src/optimize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,14 @@ export function simplifyUnion(schema: Schema, optimize: OptimizeFn): Schema {
const remainder: Schema[] = [];
innerSchemas.forEach((innerSchema) => {
if (isPrimitive(innerSchema) && innerSchema.enum !== undefined) {
innerSchema.enum.forEach((value) => {
literals[innerSchema.type].add(value);
});
// If this enum has a comment, preserve it in the union instead of consolidating
if (innerSchema.comment) {
remainder.push(innerSchema);
} else {
innerSchema.enum.forEach((value) => {
literals[innerSchema.type].add(value);
});
}
} else {
remainder.push(innerSchema);
}
Expand Down
315 changes: 315 additions & 0 deletions packages/openapi-generator/test/openapi/comments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1440,3 +1440,318 @@ testCase(
},
},
);

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

/**
* Transaction Request State Enum with individual descriptions
*/
export const TransactionRequestState = t.keyof(
{
/** Transaction is waiting for approval from authorized users */
pendingApproval: 1,
/** Transaction was canceled by the user */
canceled: 1,
/** Transaction was rejected by approvers */
rejected: 1,
/** Transaction has been initialized but not yet processed */
initialized: 1,
/** Transaction is ready to be delivered */
pendingDelivery: 1,
/** Transaction has been successfully delivered */
delivered: 1,
},
'TransactionRequestState',
);

/**
* Route to test individual enum variant descriptions
*
* @operationId api.v1.enumVariantDescriptions
* @tag Test Routes
*/
export const route = h.httpRoute({
path: '/transactions',
method: 'GET',
request: h.httpRequest({
query: {
states: t.array(TransactionRequestState),
},
}),
response: {
200: {
result: t.string
}
},
});
`;

testCase(
'individual enum variant descriptions are preserved in oneOf',
ROUTE_WITH_INDIVIDUAL_ENUM_DESCRIPTIONS,
{
openapi: '3.0.3',
info: {
title: 'Test',
version: '1.0.0',
},
paths: {
'/transactions': {
get: {
summary: 'Route to test individual enum variant descriptions',
operationId: 'api.v1.enumVariantDescriptions',
tags: ['Test Routes'],
parameters: [
{
name: 'states',
description:
'Transaction Request State Enum with individual descriptions',
in: 'query',
required: true,
schema: {
type: 'array',
items: {
oneOf: [
{
type: 'string',
enum: ['pendingApproval'],
description:
'Transaction is waiting for approval from authorized users',
},
{
type: 'string',
enum: ['canceled'],
description: 'Transaction was canceled by the user',
},
{
type: 'string',
enum: ['rejected'],
description: 'Transaction was rejected by approvers',
},
{
type: 'string',
enum: ['initialized'],
description:
'Transaction has been initialized but not yet processed',
},
{
type: 'string',
enum: ['pendingDelivery'],
description: 'Transaction is ready to be delivered',
},
{
type: 'string',
enum: ['delivered'],
description: 'Transaction has been successfully delivered',
},
],
},
},
},
],
responses: {
200: {
description: 'OK',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
result: {
type: 'string',
},
},
required: ['result'],
},
},
},
},
},
},
},
},
components: {
schemas: {
TransactionRequestState: {
title: 'TransactionRequestState',
description: 'Transaction Request State Enum with individual descriptions',
oneOf: [
{
type: 'string',
enum: ['pendingApproval'],
description: 'Transaction is waiting for approval from authorized users',
},
{
type: 'string',
enum: ['canceled'],
description: 'Transaction was canceled by the user',
},
{
type: 'string',
enum: ['rejected'],
description: 'Transaction was rejected by approvers',
},
{
type: 'string',
enum: ['initialized'],
description: 'Transaction has been initialized but not yet processed',
},
{
type: 'string',
enum: ['pendingDelivery'],
description: 'Transaction is ready to be delivered',
},
{
type: 'string',
enum: ['delivered'],
description: 'Transaction has been successfully delivered',
},
],
},
},
},
},
);

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

/**
* This is a big test of TESTINESS
*/
export const TransactionRequestState = t.keyof(
{
pendingApproval: 1,
canceled: 1,
rejected: 1,
initialized: 1,
pendingDelivery: 1,
delivered: 1,
pendingUserSignature: 1,
pendingUserCommitment: 1,
pendingUserRShare: 1,
pendingUserGShare: 1,
readyToSend: 1,
signed: 1,
failed: 1,
},
'TransactionRequestState',
);

/**
* A route that demonstrates enum descriptions being moved to parameter level
*
* @operationId api.v1.enumTest
* @tag Test Routes
*/
export const route = h.httpRoute({
path: '/transactions',
method: 'GET',
request: h.httpRequest({
query: {
states: t.array(TransactionRequestState),
},
}),
response: {
200: {
result: t.string
}
},
});
`;

testCase(
'route with enum array parameter descriptions moved to parameter level',
ROUTE_WITH_ENUM_ARRAY_PARAMETER_DESCRIPTIONS,
{
openapi: '3.0.3',
info: {
title: 'Test',
version: '1.0.0',
},
paths: {
'/transactions': {
get: {
summary:
'A route that demonstrates enum descriptions being moved to parameter level',
operationId: 'api.v1.enumTest',
tags: ['Test Routes'],
parameters: [
{
name: 'states',
description: 'This is a big test of TESTINESS',
in: 'query',
required: true,
schema: {
type: 'array',
items: {
type: 'string',
enum: [
'pendingApproval',
'canceled',
'rejected',
'initialized',
'pendingDelivery',
'delivered',
'pendingUserSignature',
'pendingUserCommitment',
'pendingUserRShare',
'pendingUserGShare',
'readyToSend',
'signed',
'failed',
],
},
},
},
],
responses: {
200: {
description: 'OK',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
result: {
type: 'string',
},
},
required: ['result'],
},
},
},
},
},
},
},
},
components: {
schemas: {
TransactionRequestState: {
title: 'TransactionRequestState',
type: 'string',
enum: [
'pendingApproval',
'canceled',
'rejected',
'initialized',
'pendingDelivery',
'delivered',
'pendingUserSignature',
'pendingUserCommitment',
'pendingUserRShare',
'pendingUserGShare',
'readyToSend',
'signed',
'failed',
],
description: 'This is a big test of TESTINESS',
},
},
},
},
);
Loading