Skip to content

Commit

Permalink
feat: support for named tupple members
Browse files Browse the repository at this point in the history
fix: broken code of conduct documentation link
fix: code style formatting
fix: corrected error message
  • Loading branch information
filipomar authored and Filipe Pomar committed May 25, 2022
1 parent aad6e59 commit 7cf221d
Show file tree
Hide file tree
Showing 10 changed files with 196 additions and 16 deletions.
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"
}
}
}

0 comments on commit 7cf221d

Please sign in to comment.