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 56f75d2
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 9 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
17 changes: 12 additions & 5 deletions src/TypeFormatter/TupleTypeFormatter.ts
Expand Up @@ -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:
Expand All @@ -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) {
Expand All @@ -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",
Expand All @@ -59,6 +65,7 @@ export class TupleTypeFormatter implements SubTypeFormatter {
...(!restDefinition && 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
11 changes: 11 additions & 0 deletions 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[][]];
114 changes: 114 additions & 0 deletions 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"
}
}
}

0 comments on commit 56f75d2

Please sign in to comment.