From 56f75d2be813f7f56244c79cc9b52aa4c03450fe Mon Sep 17 00:00:00 2001 From: Filipe Pomar Date: Sun, 1 May 2022 21:07:25 +0200 Subject: [PATCH] feat: support for named tupple members fix: broken code of conduct documentation link fix: code style formatting fix: corrected error message --- README.md | 2 +- factory/parser.ts | 2 + src/Error/UnknownNodeError.ts | 2 +- src/NodeParser/NamedTupleMemberNodeParser.ts | 26 ++++ src/Type/RestType.ts | 8 +- src/TypeFormatter/TupleTypeFormatter.ts | 17 ++- test/valid-data-type.test.ts | 1 + .../type-named-tuple-member/main.ts | 11 ++ .../type-named-tuple-member/schema.json | 114 ++++++++++++++++++ 9 files changed, 174 insertions(+), 9 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/TupleTypeFormatter.ts b/src/TypeFormatter/TupleTypeFormatter.ts index 2ff7b0cf5..8c887e76d 100644 --- a/src/TypeFormatter/TupleTypeFormatter.ts +++ b/src/TypeFormatter/TupleTypeFormatter.ts @@ -11,16 +11,17 @@ 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 restElements = subTypes.filter((t): t is RestType => t instanceof RestType); + const restType = restElements[0]; const firstItemType = requiredElements.length > 0 ? requiredElements[0] : optionalElements[0]?.getType(); // Check whether the tuple is of any of the following forms: @@ -32,7 +33,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,7 +48,12 @@ 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 restDefinition = restType + ? { + ...this.childTypeFormatter.getDefinition(restType.getType().getItem()), + ...(restType.getTitle() ? { title: restType.getTitle() as string } : {}), + } + : undefined; return { type: "array", @@ -59,6 +65,7 @@ export class TupleTypeFormatter implements SubTypeFormatter { ...(!restDefinition && 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..244e27861 --- /dev/null +++ b/test/valid-data/type-named-tuple-member/main.ts @@ -0,0 +1,11 @@ +export type MyNamedUniformTuple = [first: string, second: string]; + +export type MyNamedTuple = [first: string, second: number]; + +export type MyUniformTupleWithRest = [first: number, second: number, ...third: number[]]; + +export type MyTupleWithRest = [first: string, second: number, ...third: string[]]; + +export type MyNestedArrayWithinTuple = [first: string, second: number, third: string[]]; + +export type MyNestedArrayWithinTupleWithRest = [first: string, second: number, ...third: string[][]]; 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..e411a6a27 --- /dev/null +++ b/test/valid-data/type-named-tuple-member/schema.json @@ -0,0 +1,114 @@ +{ + "$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" + }, + "MyUniformTupleWithRest": { + "additionalItems": { + "title": "third", + "type": "number" + }, + "items": [ + { + "title": "first", + "type": "number" + }, + { + "title": "second", + "type": "number" + } + ], + "minItems": 2, + "type": "array" + }, + "MyTupleWithRest": { + "additionalItems": { + "title": "third", + "type": "string" + }, + "items": [ + { + "title": "first", + "type": "string" + }, + { + "title": "second", + "type": "number" + } + ], + "minItems": 2, + "type": "array" + }, + "MyNestedArrayWithinTuple": { + "items": [ + { + "title": "first", + "type": "string" + }, + { + "title": "second", + "type": "number" + }, + { + "items": { + "type": "string" + }, + "title": "third", + "type": "array" + } + ], + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + "MyNestedArrayWithinTupleWithRest": { + "additionalItems": { + "title": "third", + "items": { + "type": "string" + }, + "type": "array" + }, + "items": [ + { + "title": "first", + "type": "string" + }, + { + "title": "second", + "type": "number" + } + ], + "minItems": 2, + "type": "array" + } + } +}