From f7ba9d6c0643dc15690b9a5983d2c1d3da791fb7 Mon Sep 17 00:00:00 2001 From: Hadrien Milano Date: Tue, 5 Apr 2022 14:54:49 +0200 Subject: [PATCH 1/5] feat: support $ref --- .../BasicAnnotationsReader.ts | 5 +++-- src/Utils/removeUnreachable.ts | 12 +++++++++--- test/valid-data-annotations.test.ts | 2 ++ test/valid-data/annotation-ref/main.ts | 12 ++++++++++++ test/valid-data/annotation-ref/schema.json | 18 ++++++++++++++++++ 5 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 test/valid-data/annotation-ref/main.ts create mode 100644 test/valid-data/annotation-ref/schema.json diff --git a/src/AnnotationsReader/BasicAnnotationsReader.ts b/src/AnnotationsReader/BasicAnnotationsReader.ts index b19a015be..1171f6eed 100644 --- a/src/AnnotationsReader/BasicAnnotationsReader.ts +++ b/src/AnnotationsReader/BasicAnnotationsReader.ts @@ -5,7 +5,7 @@ import { Annotations } from "../Type/AnnotatedType"; import { symbolAtNode } from "../Utils/symbolAtNode"; export class BasicAnnotationsReader implements AnnotationsReader { - private static requiresDollar = new Set(["id", "comment"]); + private static requiresDollar = new Set(["id", "comment", "ref"]); private static textTags = new Set([ "title", "description", @@ -13,6 +13,7 @@ export class BasicAnnotationsReader implements AnnotationsReader { "format", "pattern", + "ref", // New since draft-07: "comment", @@ -56,7 +57,7 @@ export class BasicAnnotationsReader implements AnnotationsReader { "deprecated", ]); - public constructor(private extraTags?: Set) {} + public constructor(private extraTags?: Set) { } public getAnnotations(node: ts.Node): Annotations | undefined { const symbol = symbolAtNode(node); diff --git a/src/Utils/removeUnreachable.ts b/src/Utils/removeUnreachable.ts index cc805223f..537cf38bb 100644 --- a/src/Utils/removeUnreachable.ts +++ b/src/Utils/removeUnreachable.ts @@ -2,6 +2,8 @@ import { JSONSchema7Definition } from "json-schema"; import { Definition } from "../Schema/Definition"; import { StringMap } from "./StringMap"; +const DEFINITION_OFFSET = "#/definitions/".length; + function addReachable( definition: Definition | JSONSchema7Definition, definitions: StringMap, @@ -12,9 +14,9 @@ function addReachable( } if (definition.$ref) { - const typeName = decodeURIComponent(definition.$ref.slice(14)); - if (reachable.has(typeName)) { - // we've already processed this definition + const typeName = decodeURIComponent(definition.$ref.slice(DEFINITION_OFFSET)); + if (reachable.has(typeName) || !isLocalRef(definition.$ref)) { + // we've already processed this definition, or this definition refers to an external schema return; } reachable.add(typeName); @@ -79,3 +81,7 @@ export function removeUnreachable( return out; } + +function isLocalRef(ref: string) { + return ref.charAt(0) === "#"; +} diff --git a/test/valid-data-annotations.test.ts b/test/valid-data-annotations.test.ts index 5f5243c24..182b3da16 100644 --- a/test/valid-data-annotations.test.ts +++ b/test/valid-data-annotations.test.ts @@ -39,5 +39,7 @@ describe("valid-data-annotations", () => { it("annotation-readOnly", assertValidSchema("annotation-readOnly", "MyObject", "basic")); + it("annotation-ref", assertValidSchema("annotation-ref", "MyObject", "extended")); + it("annotation-writeOnly", assertValidSchema("annotation-writeOnly", "MyObject", "basic")); }); diff --git a/test/valid-data/annotation-ref/main.ts b/test/valid-data/annotation-ref/main.ts new file mode 100644 index 000000000..6e858a1a9 --- /dev/null +++ b/test/valid-data/annotation-ref/main.ts @@ -0,0 +1,12 @@ + +export interface MyObject { + /** + * @ref http://json-schema.org/draft-07/schema# + */ + nested: MyNestedObject +} + +export interface MyNestedObject { + foo: string; + bar: number; +} diff --git a/test/valid-data/annotation-ref/schema.json b/test/valid-data/annotation-ref/schema.json new file mode 100644 index 000000000..7e9377da1 --- /dev/null +++ b/test/valid-data/annotation-ref/schema.json @@ -0,0 +1,18 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "nested": { + "$ref": "http://json-schema.org/draft-07/schema#" + } + }, + "required": [ + "nested" + ], + "type": "object" + } + } +} From 31486e283b7605990821fae2cbff15e3879cf945 Mon Sep 17 00:00:00 2001 From: Hadrien Milano Date: Tue, 5 Apr 2022 14:56:04 +0200 Subject: [PATCH 2/5] Update src/AnnotationsReader/BasicAnnotationsReader.ts --- src/AnnotationsReader/BasicAnnotationsReader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AnnotationsReader/BasicAnnotationsReader.ts b/src/AnnotationsReader/BasicAnnotationsReader.ts index 1171f6eed..568a26189 100644 --- a/src/AnnotationsReader/BasicAnnotationsReader.ts +++ b/src/AnnotationsReader/BasicAnnotationsReader.ts @@ -57,7 +57,7 @@ export class BasicAnnotationsReader implements AnnotationsReader { "deprecated", ]); - public constructor(private extraTags?: Set) { } + public constructor(private extraTags?: Set) {} public getAnnotations(node: ts.Node): Annotations | undefined { const symbol = symbolAtNode(node); From d2a599f8635e4bbfc4f0df2f40a6a2f0ccbb88d7 Mon Sep 17 00:00:00 2001 From: Hadrien Milano Date: Wed, 6 Apr 2022 11:23:16 +0200 Subject: [PATCH 3/5] round 2 --- src/NodeParser/AnnotatedNodeParser.ts | 15 ++++++++++----- src/TypeFormatter/AnnotatedTypeFormatter.ts | 8 +++++++- test/valid-data/annotation-ref/main.ts | 5 +++++ test/valid-data/annotation-ref/schema.json | 5 ++++- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/NodeParser/AnnotatedNodeParser.ts b/src/NodeParser/AnnotatedNodeParser.ts index b6f015c6e..ae4b5534d 100644 --- a/src/NodeParser/AnnotatedNodeParser.ts +++ b/src/NodeParser/AnnotatedNodeParser.ts @@ -9,6 +9,7 @@ import { ReferenceType } from "../Type/ReferenceType"; import { removeUndefined } from "../Utils/removeUndefined"; import { DefinitionType } from "../Type/DefinitionType"; import { UnionType } from "../Type/UnionType"; +import { AnyType } from "../Type/AnyType"; export class AnnotatedNodeParser implements SubNodeParser { public constructor(protected childNodeParser: SubNodeParser, protected annotationsReader: AnnotationsReader) {} @@ -18,15 +19,21 @@ export class AnnotatedNodeParser implements SubNodeParser { } public createType(node: ts.Node, context: Context, reference?: ReferenceType): BaseType | undefined { + const annotatedNode = this.getAnnotatedNode(node); + let annotations = this.annotationsReader.getAnnotations(annotatedNode); + const nullable = this.getNullable(annotatedNode); + + // Short-circuit parsing the underlying type if an explicit ref annotation was passed. + if (annotations && "$ref" in annotations) { + return new AnnotatedType(new AnyType(), annotations, nullable); + } + const baseType = this.childNodeParser.createType(node, context, reference); if (baseType === undefined) { return undefined; } - const annotatedNode = this.getAnnotatedNode(node); - let annotations = this.annotationsReader.getAnnotations(annotatedNode); - // Don't return annotations for lib types such as Exclude. if (node.getSourceFile().fileName.match(/[/\\]typescript[/\\]lib[/\\]lib\.[^/\\]+\.d\.ts$/i)) { let specialCase = false; @@ -58,8 +65,6 @@ export class AnnotatedNodeParser implements SubNodeParser { } } - const nullable = this.getNullable(annotatedNode); - return !annotations && !nullable ? baseType : new AnnotatedType(baseType, annotations || {}, nullable); } diff --git a/src/TypeFormatter/AnnotatedTypeFormatter.ts b/src/TypeFormatter/AnnotatedTypeFormatter.ts index 854c876e5..141cc9943 100644 --- a/src/TypeFormatter/AnnotatedTypeFormatter.ts +++ b/src/TypeFormatter/AnnotatedTypeFormatter.ts @@ -50,9 +50,15 @@ export class AnnotatedTypeFormatter implements SubTypeFormatter { return type instanceof AnnotatedType; } public getDefinition(type: AnnotatedType): Definition { + const annotations = type.getAnnotations(); + + if ("$ref" in annotations) { + return annotations; + } + const def: Definition = { ...this.childTypeFormatter.getDefinition(type.getType()), - ...type.getAnnotations(), + ...annotations, }; if ("$ref" in def && "type" in def) { diff --git a/test/valid-data/annotation-ref/main.ts b/test/valid-data/annotation-ref/main.ts index 6e858a1a9..42d796e18 100644 --- a/test/valid-data/annotation-ref/main.ts +++ b/test/valid-data/annotation-ref/main.ts @@ -4,6 +4,11 @@ export interface MyObject { * @ref http://json-schema.org/draft-07/schema# */ nested: MyNestedObject + + /** + * @ref http://json-schema.org/draft-07/schema# + */ + myObject: { [key: string]: string }; } export interface MyNestedObject { diff --git a/test/valid-data/annotation-ref/schema.json b/test/valid-data/annotation-ref/schema.json index 7e9377da1..6783126fd 100644 --- a/test/valid-data/annotation-ref/schema.json +++ b/test/valid-data/annotation-ref/schema.json @@ -7,10 +7,13 @@ "properties": { "nested": { "$ref": "http://json-schema.org/draft-07/schema#" + }, + "myObject": { + "$ref": "http://json-schema.org/draft-07/schema#" } }, "required": [ - "nested" + "nested", "myObject" ], "type": "object" } From f97459c4bac5cecbb9ce058fc471e4eae79dc357 Mon Sep 17 00:00:00 2001 From: Hadrien Milano Date: Wed, 6 Apr 2022 11:27:42 +0200 Subject: [PATCH 4/5] undo useless change --- src/TypeFormatter/AnnotatedTypeFormatter.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/TypeFormatter/AnnotatedTypeFormatter.ts b/src/TypeFormatter/AnnotatedTypeFormatter.ts index 141cc9943..854c876e5 100644 --- a/src/TypeFormatter/AnnotatedTypeFormatter.ts +++ b/src/TypeFormatter/AnnotatedTypeFormatter.ts @@ -50,15 +50,9 @@ export class AnnotatedTypeFormatter implements SubTypeFormatter { return type instanceof AnnotatedType; } public getDefinition(type: AnnotatedType): Definition { - const annotations = type.getAnnotations(); - - if ("$ref" in annotations) { - return annotations; - } - const def: Definition = { ...this.childTypeFormatter.getDefinition(type.getType()), - ...annotations, + ...type.getAnnotations(), }; if ("$ref" in def && "type" in def) { From 3d7a019f67dad2d81ed2fe65a52a831a5e0b7d3c Mon Sep 17 00:00:00 2001 From: Hadrien Milano Date: Thu, 7 Apr 2022 09:50:54 +0200 Subject: [PATCH 5/5] add other tags in test --- test/valid-data/annotation-ref/main.ts | 6 ++++++ test/valid-data/annotation-ref/schema.json | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/test/valid-data/annotation-ref/main.ts b/test/valid-data/annotation-ref/main.ts index 42d796e18..740306ab8 100644 --- a/test/valid-data/annotation-ref/main.ts +++ b/test/valid-data/annotation-ref/main.ts @@ -1,11 +1,17 @@ export interface MyObject { /** + * Nested description + * + * @title Nested title * @ref http://json-schema.org/draft-07/schema# */ nested: MyNestedObject /** + * MyObject description + * + * @title MyObject title * @ref http://json-schema.org/draft-07/schema# */ myObject: { [key: string]: string }; diff --git a/test/valid-data/annotation-ref/schema.json b/test/valid-data/annotation-ref/schema.json index 6783126fd..ee0f42339 100644 --- a/test/valid-data/annotation-ref/schema.json +++ b/test/valid-data/annotation-ref/schema.json @@ -6,10 +6,14 @@ "additionalProperties": false, "properties": { "nested": { - "$ref": "http://json-schema.org/draft-07/schema#" + "$ref": "http://json-schema.org/draft-07/schema#", + "title": "Nested title", + "description": "Nested description" }, "myObject": { - "$ref": "http://json-schema.org/draft-07/schema#" + "$ref": "http://json-schema.org/draft-07/schema#", + "title": "MyObject title", + "description": "MyObject description" } }, "required": [