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
13 changes: 13 additions & 0 deletions packages/openapi-generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,9 @@ These are some tags that you can use in your schema JSDocs are custom to this ge
will have `x-internal: true` for schemas with the `@private` tag.
- `@deprecated` allows to mark any field in any schema as deprecated. The final spec
will include `deprecated: true` in the final specificaiton.
- `@preserveLineBreaks` preserves line breaks in descriptions. By default, multiline
descriptions are collapsed into a single line. Use this tag when you need to preserve
formatting, such as for markdown lists or structured text.

```typescript
import * as t from 'io-ts';
Expand All @@ -482,5 +485,15 @@ const Schema = t.type({
/** @deprecated */
deprecatedField: t.string,
publicNonDeprecatedField: t.string,
/**
* @preserveLineBreaks
* This description spans multiple lines
* and will preserve its line breaks.
*
* Available options:
* - `option1` - First option
* - `option2` - Second option
*/
fieldWithFormattedDescription: t.string,
});
```
12 changes: 11 additions & 1 deletion packages/openapi-generator/src/comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,17 @@ export function leadingComment(
commentString = commentString + endingSubstring;
}

const parsedComment = parseComment(commentString, { spacing: 'preserve' });
const shouldPreserveLineBreaks = commentString.includes('@preserveLineBreaks');
if (shouldPreserveLineBreaks) {
// This handles both inline and separate line cases
commentString = commentString.replace(/^\s*\*\s*@preserveLineBreaks\s*$/gm, '');
commentString = commentString.replace(/@preserveLineBreaks\s*/g, '');
}

const parsedComment = parseComment(
commentString,
shouldPreserveLineBreaks ? { spacing: 'preserve' } : undefined,
);

for (const block of parsedComment) {
block.description = block.description.trim();
Expand Down
148 changes: 115 additions & 33 deletions packages/openapi-generator/test/openapi/comments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1658,7 +1658,7 @@ testCase(
},
);

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

Expand All @@ -1674,6 +1674,7 @@ export const route = h.httpRoute({
request: h.httpRequest({
query: {
/**
* @preserveLineBreaks
* The permissions granted by this access token.
*
* - \`all\` - Access all actions in the test environment.
Expand All @@ -1688,46 +1689,127 @@ export const route = h.httpRoute({
});
`;

testCase('route with markdown list in comment', ROUTE_WITH_MARKDOWN_LIST, {
openapi: '3.0.3',
info: {
title: 'Test',
version: '1.0.0',
},
paths: {
'/list': {
get: {
summary: 'A route with a list in the comment',
operationId: 'api.v1.list',
tags: ['Test Routes'],
parameters: [
{
name: 'permissions',
in: 'query',
required: true,
schema: {
type: 'string',
testCase(
'route with markdown list in comment and @preserveLineBreaks tag',
ROUTE_WITH_MARKDOWN_LIST_AND_PRESERVE_LINE_BREAKS,
{
openapi: '3.0.3',
info: {
title: 'Test',
version: '1.0.0',
},
paths: {
'/list': {
get: {
summary: 'A route with a list in the comment',
operationId: 'api.v1.list',
tags: ['Test Routes'],
parameters: [
{
name: 'permissions',
in: 'query',
required: true,
schema: {
type: 'string',
},
description:
'The permissions granted by this access token.\n\n- `all` - Access all actions in the test environment.\n- `crypto_compare` - Call CryptoCompare API.',
},
description:
'The permissions granted by this access token.\n\n- `all` - Access all actions in the test environment.\n- `crypto_compare` - Call CryptoCompare API.',
},
],
responses: {
200: {
description: 'OK',
content: {
'application/json': {
schema: {
type: 'string',
],
responses: {
200: {
description: 'OK',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
components: {
schemas: {},
},
},
components: {
schemas: {},
);

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

/**
* A route with inline preserveLineBreaks tag
*
* @operationId api.v1.inline
* @tag Test Routes
*/
export const route = h.httpRoute({
path: '/inline',
method: 'GET',
request: h.httpRequest({
query: {
/**
* @preserveLineBreaks This tag is inline with other text
* This is a long description that
* spans multiple lines in the source.
*/
field1: t.string,
},
}),
response: {
200: t.string
},
});
`;

testCase(
'route with inline @preserveLineBreaks tag',
ROUTE_WITH_INLINE_PRESERVE_LINE_BREAKS,
{
openapi: '3.0.3',
info: {
title: 'Test',
version: '1.0.0',
},
paths: {
'/inline': {
get: {
summary: 'A route with inline preserveLineBreaks tag',
operationId: 'api.v1.inline',
tags: ['Test Routes'],
parameters: [
{
name: 'field1',
in: 'query',
required: true,
schema: {
type: 'string',
},
description:
'This tag is inline with other text\nThis is a long description that\nspans multiple lines in the source.',
},
],
responses: {
200: {
description: 'OK',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
components: {
schemas: {},
},
},
);