Skip to content

Commit

Permalink
feat(openapi-generator): take and parse @example tags on JSDoc
Browse files Browse the repository at this point in the history
  • Loading branch information
ekorenblum-simtlix committed May 2, 2024
1 parent b80f434 commit 9da14c6
Show file tree
Hide file tree
Showing 4 changed files with 353 additions and 11 deletions.
43 changes: 33 additions & 10 deletions packages/openapi-generator/src/jsdoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,49 @@ import type { Block } from 'comment-parser';
export type JSDoc = {
summary?: string;
description?: string;
tags?: Record<string, string>;
tags?: Record<string, string> & { example?: unknown };
};

export function parseCommentBlock(comment: Block): JSDoc {
let summary: string = '';
let description: string = '';
let tags: Record<string, string> = {};
let tags: Record<string, string> & { example?: any } = {};
let writingExample = false;

for (const line of comment.source) {
if (summary.length === 0) {
if (line.tokens.description === '') {
continue;
if (writingExample) {
tags['example'] = `${tags['example']}\n${line.source.split('*')[1]?.trim()}`;
try {
tags['example'] = JSON.parse(tags['example']);
writingExample = false;
} catch (e) {
if (line.source.endsWith('*/'))
throw new Error('@example contains invalid JSON');
else continue;
}
summary = line.tokens.description;
} else {
if (line.tokens.tag !== undefined && line.tokens.tag.length > 0) {
tags[line.tokens.tag.slice(1)] =
`${line.tokens.name} ${line.tokens.description}`.trim();
if (summary.length === 0) {
if (line.tokens.description === '') {
continue;
}
summary = line.tokens.description;
} else {
description = `${description ?? ''}\n${line.tokens.description}`;
if (line.tokens.tag !== undefined && line.tokens.tag.length > 0) {
if (line.tokens.tag === '@example') {
tags['example'] = line.source.split('@example')[1]?.trim();
if (tags['example'].startsWith('{') || tags['example'].startsWith('[')) {
try {
tags['example'] = JSON.parse(tags['example']);
} catch (e) {
writingExample = true;
}
}
} else
tags[line.tokens.tag.slice(1)] =
`${line.tokens.name} ${line.tokens.description}`.trim();
} else {
description = `${description ?? ''}\n${line.tokens.description}`;
}
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion packages/openapi-generator/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObjec
const tag = jsdoc.tags?.tag ?? '';
const isInternal = jsdoc.tags?.private !== undefined;
const isUnstable = jsdoc.tags?.unstable !== undefined;
const example = jsdoc.tags?.example;

const requestBody =
route.body === undefined
Expand Down Expand Up @@ -163,7 +164,10 @@ function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObjec
[Number(code)]: {
description,
content: {
'application/json': { schema: schemaToOpenAPI(response) },
'application/json': {
schema: schemaToOpenAPI(response),
...(example !== undefined ? { example } : undefined),
},
},
},
};
Expand Down
124 changes: 124 additions & 0 deletions packages/openapi-generator/test/jsdoc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,127 @@ test('comment with a summary, description, and a tag in the middle of the descri

assert.deepEqual(parseJSDoc(comment), expected);
});

test('parameter with a comment and an example string', () => {
const comment = `
/**
* A variable with example
*
* @example foo
*/
`;

const expected: JSDoc = {
summary: 'A variable with example',
tags: {
example: 'foo',
},
};

assert.deepEqual(parseJSDoc(comment), expected);
});

test('parameter with a comment and an example object', () => {
const comment = `
/**
* A variable with example
*
* @example { "test": "foo" }
*/
`;

const expected: JSDoc = {
summary: 'A variable with example',
// @ts-expect-error parser doesn't properly infer type
tags: {
example: { test: 'foo' },
},
};

assert.deepEqual(parseJSDoc(comment), expected);
});

test('parameter with a comment and an example object (multi-line)', () => {
const comment = `
/**
* A variable with example
*
* @example {
* "test": "foo"
* }
*/
`;

const expected: JSDoc = {
summary: 'A variable with example',
// @ts-expect-error parser doesn't properly infer type
tags: {
example: { test: 'foo' },
},
};

assert.deepEqual(parseJSDoc(comment), expected);
});

test('parameter with a comment and an example array', () => {
const comment = `
/**
* A variable with example
*
* @example ["foo", "bar", "baz"]
*/
`;

const expected: JSDoc = {
summary: 'A variable with example',
// @ts-expect-error parser doesn't properly infer type
tags: {
example: ['foo', 'bar', 'baz'],
},
};

assert.deepEqual(parseJSDoc(comment), expected);
});

test('parameter with a comment and an invalid example object', () => {
const comment = `
/**
* A variable with example
*
* @example { "test": "foo"
*/
`;

assert.throws(() => parseJSDoc(comment), {
message: '@example contains invalid JSON',
});
});

test('parameter with a comment and an invalid example object (multi-line)', () => {
const comment = `
/**
* A variable with example
*
* @example {
* "test": "foo"
*/
`;

assert.throws(() => parseJSDoc(comment), {
message: '@example contains invalid JSON',
});
});

test('parameter with a comment and an invalid example array', () => {
const comment = `
/**
* A variable with example
*
* @example ["foo", "bar", "baz"
*/
`;

assert.throws(() => parseJSDoc(comment), {
message: '@example contains invalid JSON',
});
});
Loading

0 comments on commit 9da14c6

Please sign in to comment.