diff --git a/src/AnnotationsReader/ExtendedAnnotationsReader.ts b/src/AnnotationsReader/ExtendedAnnotationsReader.ts index 4edc7ae9e..6dc2b3081 100644 --- a/src/AnnotationsReader/ExtendedAnnotationsReader.ts +++ b/src/AnnotationsReader/ExtendedAnnotationsReader.ts @@ -1,3 +1,4 @@ +import json5 from "json5"; import ts from "typescript"; import { Annotations } from "../Type/AnnotatedType"; import { symbolAtNode } from "../Utils/symbolAtNode"; @@ -12,6 +13,7 @@ export class ExtendedAnnotationsReader extends BasicAnnotationsReader { const annotations: Annotations = { ...this.getDescriptionAnnotation(node), ...this.getTypeAnnotation(node), + ...this.getExampleAnnotation(node), ...super.getAnnotations(node), }; return Object.keys(annotations).length ? annotations : undefined; @@ -70,4 +72,37 @@ export class ExtendedAnnotationsReader extends BasicAnnotationsReader { const text = (jsDocTag.text ?? []).map((part) => part.text).join(""); return { type: text }; } + /** + * Attempts to gather examples from the @-example jsdoc tag. + * See https://tsdoc.org/pages/tags/example/ + */ + private getExampleAnnotation(node: ts.Node): Annotations | undefined { + const symbol = symbolAtNode(node); + if (!symbol) { + return undefined; + } + + const jsDocTags: ts.JSDocTagInfo[] = symbol.getJsDocTags(); + if (!jsDocTags || !jsDocTags.length) { + return undefined; + } + + const examples: unknown[] = []; + for (const example of jsDocTags.filter((tag) => tag.name === "example")) { + const text = (example.text ?? []).map((part) => part.text).join(""); + try { + examples.push(json5.parse(text)); + } catch (e) { + // ignore examples which don't parse to valid JSON + // This could be improved to support a broader range of usages, + // such as if the example has a title (as explained in the tsdoc spec). + } + } + + if (examples.length === 0) { + return undefined; + } + + return { examples }; + } } diff --git a/test/valid-data-annotations.test.ts b/test/valid-data-annotations.test.ts index 4fd4b65e4..5f5243c24 100644 --- a/test/valid-data-annotations.test.ts +++ b/test/valid-data-annotations.test.ts @@ -33,6 +33,8 @@ describe("valid-data-annotations", () => { it("annotation-comment", assertValidSchema("annotation-comment", "MyObject", "extended")); + it("annotation-example", assertValidSchema("annotation-example", "MyObject", "extended")); + it("annotation-id", assertValidSchema("annotation-id", "MyObject", "extended", [], "Test")); it("annotation-readOnly", assertValidSchema("annotation-readOnly", "MyObject", "basic")); diff --git a/test/valid-data/annotation-example/main.ts b/test/valid-data/annotation-example/main.ts new file mode 100644 index 000000000..91a76ff0a --- /dev/null +++ b/test/valid-data/annotation-example/main.ts @@ -0,0 +1,32 @@ +/** + * @example + * { + * "nested": "hello" + * } + * + * @example An invalid example + * { + * "nested": "world" + * } + * @example Another invalid example + * ```ts + * { + * "nested": "world" + * } + * ``` + */ +export interface MyObject { + /** + * @example + * "Hello world" + * @example + * "This string rocks" + */ + nested: MyNestedObject +} + +/** + * @example With a string + * "Hello string" + */ +export type MyNestedObject = string; diff --git a/test/valid-data/annotation-example/schema.json b/test/valid-data/annotation-example/schema.json new file mode 100644 index 000000000..3d9a670e8 --- /dev/null +++ b/test/valid-data/annotation-example/schema.json @@ -0,0 +1,28 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyNestedObject": { + "type": "string" + }, + "MyObject": { + "examples": [ + { "nested": "hello" } + ], + "additionalProperties": false, + "properties": { + "nested": { + "examples": [ + "Hello world", + "This string rocks" + ], + "$ref": "#/definitions/MyNestedObject" + } + }, + "required": [ + "nested" + ], + "type": "object" + } + } +}