Skip to content

Commit 9af30ad

Browse files
authored
Merge pull request #753 from BitGo/DX-338-@example-tag-in-generator
feat(openapi-generator): take and parse @example tags on JSDoc
2 parents 204c4ee + bf25f3f commit 9af30ad

File tree

4 files changed

+401
-11
lines changed

4 files changed

+401
-11
lines changed

packages/openapi-generator/src/jsdoc.ts

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,53 @@
11
import type { Block } from 'comment-parser';
22

3+
type Tags = Record<Exclude<string, 'example'>, string> & { example?: any };
4+
35
export type JSDoc = {
46
summary?: string;
57
description?: string;
6-
tags?: Record<string, string>;
8+
tags?: Tags;
79
};
810

911
export function parseCommentBlock(comment: Block): JSDoc {
1012
let summary: string = '';
1113
let description: string = '';
12-
let tags: Record<string, string> = {};
14+
let tags: Tags = {};
15+
let writingExample = false;
1316

1417
for (const line of comment.source) {
15-
if (summary.length === 0) {
16-
if (line.tokens.description === '') {
17-
continue;
18+
if (writingExample) {
19+
tags['example'] = `${tags['example']}\n${line.source.split('*')[1]?.trim()}`;
20+
try {
21+
tags['example'] = JSON.parse(tags['example']);
22+
writingExample = false;
23+
} catch (e) {
24+
if (line.source.endsWith('*/'))
25+
throw new Error('@example contains invalid JSON');
26+
else continue;
1827
}
19-
summary = line.tokens.description;
2028
} else {
21-
if (line.tokens.tag !== undefined && line.tokens.tag.length > 0) {
22-
tags[line.tokens.tag.slice(1)] =
23-
`${line.tokens.name} ${line.tokens.description}`.trim();
29+
if (summary.length === 0) {
30+
if (line.tokens.description === '') {
31+
continue;
32+
}
33+
summary = line.tokens.description;
2434
} else {
25-
description = `${description ?? ''}\n${line.tokens.description}`;
35+
if (line.tokens.tag !== undefined && line.tokens.tag.length > 0) {
36+
if (line.tokens.tag === '@example') {
37+
tags['example'] = line.source.split('@example')[1]?.trim();
38+
if (tags['example'].startsWith('{') || tags['example'].startsWith('[')) {
39+
try {
40+
tags['example'] = JSON.parse(tags['example']);
41+
} catch (e) {
42+
writingExample = true;
43+
}
44+
}
45+
} else
46+
tags[line.tokens.tag.slice(1)] =
47+
`${line.tokens.name} ${line.tokens.description}`.trim();
48+
} else {
49+
description = `${description ?? ''}\n${line.tokens.description}`;
50+
}
2651
}
2752
}
2853
}

packages/openapi-generator/src/openapi.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObjec
117117
const tag = jsdoc.tags?.tag ?? '';
118118
const isInternal = jsdoc.tags?.private !== undefined;
119119
const isUnstable = jsdoc.tags?.unstable !== undefined;
120+
const example = jsdoc.tags?.example;
120121

121122
const requestBody =
122123
route.body === undefined
@@ -163,7 +164,10 @@ function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObjec
163164
[Number(code)]: {
164165
description,
165166
content: {
166-
'application/json': { schema: schemaToOpenAPI(response) },
167+
'application/json': {
168+
schema: schemaToOpenAPI(response),
169+
...(example !== undefined ? { example } : undefined),
170+
},
167171
},
168172
},
169173
};

packages/openapi-generator/test/jsdoc.test.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,173 @@ test('comment with a summary, description, and a tag in the middle of the descri
152152

153153
assert.deepEqual(parseJSDoc(comment), expected);
154154
});
155+
156+
test('parameter with a comment and an example string', () => {
157+
const comment = `
158+
/**
159+
* A variable with example
160+
*
161+
* @example foo
162+
*/
163+
`;
164+
165+
const expected: JSDoc = {
166+
summary: 'A variable with example',
167+
tags: {
168+
example: 'foo',
169+
},
170+
};
171+
172+
assert.deepEqual(parseJSDoc(comment), expected);
173+
});
174+
175+
test('parameter with a comment and an example object', () => {
176+
const comment = `
177+
/**
178+
* A variable with example
179+
*
180+
* @example { "test": "foo" }
181+
*/
182+
`;
183+
184+
const expected: JSDoc = {
185+
summary: 'A variable with example',
186+
// @ts-expect-error parser doesn't properly infer type
187+
tags: {
188+
example: { test: 'foo' },
189+
},
190+
};
191+
192+
assert.deepEqual(parseJSDoc(comment), expected);
193+
});
194+
195+
test('parameter with a comment and an example object (multi-line)', () => {
196+
const comment = `
197+
/**
198+
* A variable with example
199+
*
200+
* @example {
201+
* "test": "foo"
202+
* }
203+
*/
204+
`;
205+
206+
const expected: JSDoc = {
207+
summary: 'A variable with example',
208+
// @ts-expect-error parser doesn't properly infer type
209+
tags: {
210+
example: { test: 'foo' },
211+
},
212+
};
213+
214+
assert.deepEqual(parseJSDoc(comment), expected);
215+
});
216+
217+
test('parameter with a comment and an example array', () => {
218+
const comment = `
219+
/**
220+
* A variable with example
221+
*
222+
* @example ["foo", "bar", "baz"]
223+
*/
224+
`;
225+
226+
const expected: JSDoc = {
227+
summary: 'A variable with example',
228+
// @ts-expect-error parser doesn't properly infer type
229+
tags: {
230+
example: ['foo', 'bar', 'baz'],
231+
},
232+
};
233+
234+
assert.deepEqual(parseJSDoc(comment), expected);
235+
});
236+
237+
test('parameter with a comment and an invalid example object', () => {
238+
const comment = `
239+
/**
240+
* A variable with example
241+
*
242+
* @example { "test": "foo"
243+
*/
244+
`;
245+
246+
assert.throws(() => parseJSDoc(comment), {
247+
message: '@example contains invalid JSON',
248+
});
249+
});
250+
251+
test('parameter with a comment and an invalid example object (multi-line)', () => {
252+
const comment = `
253+
/**
254+
* A variable with example
255+
*
256+
* @example {
257+
* "test": "foo"
258+
*/
259+
`;
260+
261+
assert.throws(() => parseJSDoc(comment), {
262+
message: '@example contains invalid JSON',
263+
});
264+
});
265+
266+
test('parameter with a comment and an invalid example array', () => {
267+
const comment = `
268+
/**
269+
* A variable with example
270+
*
271+
* @example ["foo", "bar", "baz"
272+
*/
273+
`;
274+
275+
assert.throws(() => parseJSDoc(comment), {
276+
message: '@example contains invalid JSON',
277+
});
278+
});
279+
280+
test('parameter with a comment, an example object and a tag', () => {
281+
const comment = `
282+
/**
283+
* A variable with example
284+
*
285+
* @example { "test": "foo" }
286+
* @tag api.example.test
287+
*/
288+
`;
289+
290+
const expected: JSDoc = {
291+
summary: 'A variable with example',
292+
// @ts-expect-error parser doesn't properly infer type
293+
tags: {
294+
example: { test: 'foo' },
295+
tag: 'api.example.test',
296+
},
297+
};
298+
299+
assert.deepEqual(parseJSDoc(comment), expected);
300+
});
301+
302+
test('parameter with a comment, an example object (multi-line) and a tag', () => {
303+
const comment = `
304+
/**
305+
* A variable with example
306+
*
307+
* @example {
308+
* "test": "foo"
309+
* }
310+
* @tag api.example.test
311+
*/
312+
`;
313+
314+
const expected: JSDoc = {
315+
summary: 'A variable with example',
316+
// @ts-expect-error parser doesn't properly infer type
317+
tags: {
318+
example: { test: 'foo' },
319+
tag: 'api.example.test',
320+
},
321+
};
322+
323+
assert.deepEqual(parseJSDoc(comment), expected);
324+
});

0 commit comments

Comments
 (0)