Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support for named tuple members #1236

Merged
merged 1 commit into from May 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions factory/parser.ts
Expand Up @@ -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";
Expand Down Expand Up @@ -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))

Expand Down
2 changes: 1 addition & 1 deletion src/Error/UnknownNodeError.ts
Expand Up @@ -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 {
Expand Down
26 changes: 26 additions & 0 deletions 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);
}
}
8 changes: 6 additions & 2 deletions src/Type/RestType.ts
Expand Up @@ -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 {
Expand Down
13 changes: 11 additions & 2 deletions src/TypeFormatter/RestTypeFormatter.ts
Expand Up @@ -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());
}
Expand Down
23 changes: 13 additions & 10 deletions src/TypeFormatter/TupleTypeFormatter.ts
Expand Up @@ -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:
Expand All @@ -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) {
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions test/valid-data-type.test.ts
Expand Up @@ -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"));
Expand Down
13 changes: 13 additions & 0 deletions 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[]];
122 changes: 122 additions & 0 deletions 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"
}
}
}