From 42ce3b68aa7111539cf4934b2d94b83f08b56ad0 Mon Sep 17 00:00:00 2001 From: filipomar Date: Wed, 25 May 2022 22:34:26 +0200 Subject: [PATCH] feat: support for named tupple members (#1236) --- README.md | 2 +- factory/parser.ts | 2 + src/Error/UnknownNodeError.ts | 2 +- src/NodeParser/NamedTupleMemberNodeParser.ts | 26 ++++ src/Type/RestType.ts | 8 +- src/TypeFormatter/RestTypeFormatter.ts | 13 +- src/TypeFormatter/TupleTypeFormatter.ts | 23 ++-- test/valid-data-type.test.ts | 1 + .../type-named-tuple-member/main.ts | 13 ++ .../type-named-tuple-member/schema.json | 122 ++++++++++++++++++ 10 files changed, 196 insertions(+), 16 deletions(-) create mode 100644 src/NodeParser/NamedTupleMemberNodeParser.ts create mode 100644 test/valid-data/type-named-tuple-member/main.ts create mode 100644 test/valid-data/type-named-tuple-member/schema.json diff --git a/README.md b/README.md index 5710f68a3..2e8c76057 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Inspired by [`YousefED/typescript-json-schema`](https://github.com/YousefED/type ## Contributors -This project is made possible by a [community of contributors](https://github.com/vega/ts-json-schema-generator/graphs/contributors). We welcome contributions of any kind (issues, code, documentation, examples, tests,...). Please read our [code of conduct](https://github.com/vega/vega/blob/master/CODE_OF_CONDUCT.md). +This project is made possible by a [community of contributors](https://github.com/vega/ts-json-schema-generator/graphs/contributors). We welcome contributions of any kind (issues, code, documentation, examples, tests,...). Please read our [code of conduct](https://vega.github.io/vega/about/code-of-conduct). ## CLI Usage diff --git a/factory/parser.ts b/factory/parser.ts index b472d93a3..3ca76ab0b 100644 --- a/factory/parser.ts +++ b/factory/parser.ts @@ -27,6 +27,7 @@ import { IntersectionNodeParser } from "../src/NodeParser/IntersectionNodeParser import { IntrinsicNodeParser } from "../src/NodeParser/IntrinsicNodeParser"; import { LiteralNodeParser } from "../src/NodeParser/LiteralNodeParser"; import { MappedTypeNodeParser } from "../src/NodeParser/MappedTypeNodeParser"; +import { NamedTupleMemberNodeParser } from "../src/NodeParser/NamedTupleMemberNodeParser"; import { NeverTypeNodeParser } from "../src/NodeParser/NeverTypeNodeParser"; import { NullLiteralNodeParser } from "../src/NodeParser/NullLiteralNodeParser"; import { NumberLiteralNodeParser } from "../src/NodeParser/NumberLiteralNodeParser"; @@ -130,6 +131,7 @@ export function createParser(program: ts.Program, config: Config, augmentor?: Pa .addNodeParser(new UnionNodeParser(typeChecker, chainNodeParser)) .addNodeParser(new IntersectionNodeParser(typeChecker, chainNodeParser)) .addNodeParser(new TupleNodeParser(typeChecker, chainNodeParser)) + .addNodeParser(new NamedTupleMemberNodeParser(chainNodeParser)) .addNodeParser(new OptionalTypeNodeParser(chainNodeParser)) .addNodeParser(new RestTypeNodeParser(chainNodeParser)) diff --git a/src/Error/UnknownNodeError.ts b/src/Error/UnknownNodeError.ts index 98909bb74..03a062db5 100644 --- a/src/Error/UnknownNodeError.ts +++ b/src/Error/UnknownNodeError.ts @@ -3,7 +3,7 @@ import { BaseError } from "./BaseError"; export class UnknownNodeError extends BaseError { public constructor(private node: ts.Node, private reference?: ts.Node) { - super(`Unknown node "${node.getFullText()}`); + super(`Unknown node "${node.getFullText()}" of kind "${ts.SyntaxKind[node.kind]}"`); } public getNode(): ts.Node { diff --git a/src/NodeParser/NamedTupleMemberNodeParser.ts b/src/NodeParser/NamedTupleMemberNodeParser.ts new file mode 100644 index 000000000..2f3478d53 --- /dev/null +++ b/src/NodeParser/NamedTupleMemberNodeParser.ts @@ -0,0 +1,26 @@ +import ts from "typescript"; +import { Context, NodeParser } from "../NodeParser"; +import { SubNodeParser } from "../SubNodeParser"; +import { AnnotatedType } from "../Type/AnnotatedType"; +import { ArrayType } from "../Type/ArrayType"; +import { BaseType } from "../Type/BaseType"; +import { ReferenceType } from "../Type/ReferenceType"; +import { RestType } from "../Type/RestType"; + +export class NamedTupleMemberNodeParser implements SubNodeParser { + public constructor(protected childNodeParser: NodeParser) {} + + public supportsNode(node: ts.TypeNode): boolean { + return node.kind === ts.SyntaxKind.NamedTupleMember; + } + + public createType(node: ts.NamedTupleMember, context: Context, reference?: ReferenceType): BaseType | undefined { + const baseType = this.childNodeParser.createType(node.type, context, reference); + + if (baseType instanceof ArrayType && node.getChildAt(0).kind === ts.SyntaxKind.DotDotDotToken) { + return new RestType(baseType, node.name.text); + } + + return baseType && new AnnotatedType(baseType, { title: node.name.text }, false); + } +} diff --git a/src/Type/RestType.ts b/src/Type/RestType.ts index 49447db9f..0af9ca74c 100644 --- a/src/Type/RestType.ts +++ b/src/Type/RestType.ts @@ -2,12 +2,16 @@ import { ArrayType } from "./ArrayType"; import { BaseType } from "./BaseType"; export class RestType extends BaseType { - public constructor(private item: ArrayType) { + public constructor(private item: ArrayType, private title: string | null = null) { super(); } public getId(): string { - return `...${this.item.getId()}`; + return `...${this.item.getId()}${this.title || ""}`; + } + + public getTitle(): string | null { + return this.title; } public getType(): ArrayType { diff --git a/src/TypeFormatter/RestTypeFormatter.ts b/src/TypeFormatter/RestTypeFormatter.ts index 6b09d2aee..cc22d674e 100644 --- a/src/TypeFormatter/RestTypeFormatter.ts +++ b/src/TypeFormatter/RestTypeFormatter.ts @@ -7,12 +7,21 @@ import { TypeFormatter } from "../TypeFormatter"; export class RestTypeFormatter implements SubTypeFormatter { public constructor(protected childTypeFormatter: TypeFormatter) {} - public supportsType(type: RestType): boolean { + public supportsType(type: BaseType): boolean { return type instanceof RestType; } + public getDefinition(type: RestType): Definition { - return this.childTypeFormatter.getDefinition(type.getType()); + const definition = this.childTypeFormatter.getDefinition(type.getType()); + const title = type.getTitle(); + + if (title !== null && typeof definition.items === "object") { + return { ...definition, items: { ...definition.items, title } }; + } + + return definition; } + public getChildren(type: RestType): BaseType[] { return this.childTypeFormatter.getChildren(type.getType()); } diff --git a/src/TypeFormatter/TupleTypeFormatter.ts b/src/TypeFormatter/TupleTypeFormatter.ts index 2ff7b0cf5..53a1cbda3 100644 --- a/src/TypeFormatter/TupleTypeFormatter.ts +++ b/src/TypeFormatter/TupleTypeFormatter.ts @@ -11,16 +11,16 @@ import { uniqueArray } from "../Utils/uniqueArray"; export class TupleTypeFormatter implements SubTypeFormatter { public constructor(protected childTypeFormatter: TypeFormatter) {} - public supportsType(type: TupleType): boolean { + public supportsType(type: BaseType): boolean { return type instanceof TupleType; } + public getDefinition(type: TupleType): Definition { const subTypes = type.getTypes().filter(notUndefined); const requiredElements = subTypes.filter((t) => !(t instanceof OptionalType) && !(t instanceof RestType)); - const optionalElements = subTypes.filter((t) => t instanceof OptionalType) as OptionalType[]; - const restElements = subTypes.filter((t) => t instanceof RestType) as RestType[]; - const restType = restElements.length ? restElements[0].getType().getItem() : undefined; + const optionalElements = subTypes.filter((t): t is OptionalType => t instanceof OptionalType); + const restType = subTypes.find((t): t is RestType => t instanceof RestType); const firstItemType = requiredElements.length > 0 ? requiredElements[0] : optionalElements[0]?.getType(); // Check whether the tuple is of any of the following forms: @@ -32,7 +32,7 @@ export class TupleTypeFormatter implements SubTypeFormatter { firstItemType && requiredElements.every((item) => item.getId() === firstItemType.getId()) && optionalElements.every((item) => item.getType().getId() === firstItemType.getId()) && - (restElements.length === 0 || (restElements.length === 1 && restType?.getId() === firstItemType.getId())); + (!restType || restType.getType().getItem().getId() === firstItemType.getId()); // If so, generate a simple array with minItems (and possibly maxItems) instead. if (isUniformArray) { @@ -47,18 +47,21 @@ export class TupleTypeFormatter implements SubTypeFormatter { const requiredDefinitions = requiredElements.map((item) => this.childTypeFormatter.getDefinition(item)); const optionalDefinitions = optionalElements.map((item) => this.childTypeFormatter.getDefinition(item)); const itemsTotal = requiredDefinitions.length + optionalDefinitions.length; - const restDefinition = restType ? this.childTypeFormatter.getDefinition(restType) : undefined; + const additionalItems = restType ? this.childTypeFormatter.getDefinition(restType).items : undefined; return { type: "array", minItems: requiredDefinitions.length, ...(itemsTotal ? { items: requiredDefinitions.concat(optionalDefinitions) } : {}), // with items - ...(!itemsTotal && restDefinition ? { items: restDefinition } : {}), // with only rest param - ...(!itemsTotal && !restDefinition ? { maxItems: 0 } : {}), // empty - ...(restDefinition && itemsTotal ? { additionalItems: restDefinition } : {}), // with items and rest - ...(!restDefinition && itemsTotal ? { maxItems: itemsTotal } : {}), // without rest + ...(!itemsTotal && additionalItems ? { items: additionalItems } : {}), // with only rest param + ...(!itemsTotal && !additionalItems ? { maxItems: 0 } : {}), // empty + ...(additionalItems && !Array.isArray(additionalItems) && itemsTotal + ? { additionalItems: additionalItems } + : {}), // with rest items + ...(!additionalItems && itemsTotal ? { maxItems: itemsTotal } : {}), // without rest }; } + public getChildren(type: TupleType): BaseType[] { return uniqueArray( type diff --git a/test/valid-data-type.test.ts b/test/valid-data-type.test.ts index 596e3cc62..1bc376d36 100644 --- a/test/valid-data-type.test.ts +++ b/test/valid-data-type.test.ts @@ -27,6 +27,7 @@ describe("valid-data-type", () => { it("type-aliases-tuple-optional-items", assertValidSchema("type-aliases-tuple-optional-items", "MyTuple")); it("type-aliases-tuple-rest", assertValidSchema("type-aliases-tuple-rest", "MyTuple")); it("type-aliases-tuple-only-rest", assertValidSchema("type-aliases-tuple-only-rest", "MyTuple")); + it("type-named-tuple-member", assertValidSchema("type-named-tuple-member", "*")); it("type-maps", assertValidSchema("type-maps", "MyObject")); it("type-primitives", assertValidSchema("type-primitives", "MyObject")); diff --git a/test/valid-data/type-named-tuple-member/main.ts b/test/valid-data/type-named-tuple-member/main.ts new file mode 100644 index 000000000..0b5613c94 --- /dev/null +++ b/test/valid-data/type-named-tuple-member/main.ts @@ -0,0 +1,13 @@ +export type MyNamedUniformTuple = [first: string, second: string]; + +export type MyNamedTuple = [first: string, second: number]; + +export type MyNamedUniformTupleWithRest = [first: number, second: number, ...third: number[]]; + +export type MyNamedTupleWithRest = [first: string, second: number, ...third: string[]]; + +export type MyNamedNestedArrayWithinTuple = [first: string, second: number, third: string[]]; + +export type MyNamedNestedArrayWithinTupleWithRest = [first: string, second: number, ...third: string[][]]; + +export type MyNamedTupleWithOnlyRest = [...first: number[]]; diff --git a/test/valid-data/type-named-tuple-member/schema.json b/test/valid-data/type-named-tuple-member/schema.json new file mode 100644 index 000000000..1ac861c63 --- /dev/null +++ b/test/valid-data/type-named-tuple-member/schema.json @@ -0,0 +1,122 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyNamedUniformTuple": { + "items": [ + { + "title": "first", + "type": "string" + }, + { + "title": "second", + "type": "string" + } + ], + "minItems": 2, + "maxItems": 2, + "type": "array" + }, + "MyNamedTuple": { + "items": [ + { + "title": "first", + "type": "string" + }, + { + "title": "second", + "type": "number" + } + ], + "maxItems": 2, + "minItems": 2, + "type": "array" + }, + "MyNamedUniformTupleWithRest": { + "additionalItems": { + "title": "third", + "type": "number" + }, + "items": [ + { + "title": "first", + "type": "number" + }, + { + "title": "second", + "type": "number" + } + ], + "minItems": 2, + "type": "array" + }, + "MyNamedTupleWithRest": { + "additionalItems": { + "title": "third", + "type": "string" + }, + "items": [ + { + "title": "first", + "type": "string" + }, + { + "title": "second", + "type": "number" + } + ], + "minItems": 2, + "type": "array" + }, + "MyNamedNestedArrayWithinTuple": { + "items": [ + { + "title": "first", + "type": "string" + }, + { + "title": "second", + "type": "number" + }, + { + "items": { + "type": "string" + }, + "title": "third", + "type": "array" + } + ], + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + "MyNamedNestedArrayWithinTupleWithRest": { + "additionalItems": { + "title": "third", + "items": { + "type": "string" + }, + "type": "array" + }, + "items": [ + { + "title": "first", + "type": "string" + }, + { + "title": "second", + "type": "number" + } + ], + "minItems": 2, + "type": "array" + }, + "MyNamedTupleWithOnlyRest": { + "items": { + "title": "first", + "type": "number" + }, + "minItems": 0, + "type": "array" + } + } +}