Skip to content

Commit

Permalink
feat: support template literals as types. (#1171)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason3S committed Mar 20, 2022
1 parent 30eba37 commit 642022c
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 0 deletions.
2 changes: 2 additions & 0 deletions factory/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { PrefixUnaryExpressionNodeParser } from "../src/NodeParser/PrefixUnaryEx
import { PropertyAccessExpressionParser } from "../src/NodeParser/PropertyAccessExpressionParser";
import { RestTypeNodeParser } from "../src/NodeParser/RestTypeNodeParser";
import { StringLiteralNodeParser } from "../src/NodeParser/StringLiteralNodeParser";
import { StringTemplateLiteralNodeParser } from "../src/NodeParser/StringTemplateLiteralNodeParser";
import { StringTypeNodeParser } from "../src/NodeParser/StringTypeNodeParser";
import { SymbolTypeNodeParser } from "../src/NodeParser/SymbolTypeNodeParser";
import { TupleNodeParser } from "../src/NodeParser/TupleNodeParser";
Expand Down Expand Up @@ -102,6 +103,7 @@ export function createParser(program: ts.Program, config: Config, augmentor?: Pa
.addNodeParser(new FunctionParser(chainNodeParser))
.addNodeParser(withJsDoc(new ParameterParser(chainNodeParser)))
.addNodeParser(new StringLiteralNodeParser())
.addNodeParser(new StringTemplateLiteralNodeParser(chainNodeParser))
.addNodeParser(new NumberLiteralNodeParser())
.addNodeParser(new BooleanLiteralNodeParser())
.addNodeParser(new NullLiteralNodeParser())
Expand Down
71 changes: 71 additions & 0 deletions src/NodeParser/StringTemplateLiteralNodeParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import ts from "typescript";
import { UnknownTypeError } from "../Error/UnknownTypeError";
import { Context, NodeParser } from "../NodeParser";
import { SubNodeParser } from "../SubNodeParser";
import { AliasType } from "../Type/AliasType";
import { BaseType } from "../Type/BaseType";
import { LiteralType } from "../Type/LiteralType";
import { UnionType } from "../Type/UnionType";

export class StringTemplateLiteralNodeParser implements SubNodeParser {
public constructor(protected childNodeParser: NodeParser) {}

public supportsNode(node: ts.NoSubstitutionTemplateLiteral | ts.TemplateLiteralTypeNode): boolean {
return (
node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral || node.kind === ts.SyntaxKind.TemplateLiteralType
);
}
public createType(node: ts.NoSubstitutionTemplateLiteral | ts.TemplateLiteralTypeNode, context: Context): BaseType {
if (node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral) {
return new LiteralType(node.text);
}
const prefix = node.head.text;
const matrix: string[][] = [[prefix]].concat(
node.templateSpans.map((span) => {
const suffix = span.literal.text;
const type = this.childNodeParser.createType(span.type, context);
return [...extractLiterals(type)].map((value) => value + suffix);
})
);

const expandedLiterals = expand(matrix);

const expandedTypes = expandedLiterals.map((literal) => new LiteralType(literal));

if (expandedTypes.length === 1) {
return expandedTypes[0];
}

return new UnionType(expandedTypes);
}
}

function expand(matrix: string[][]): string[] {
if (matrix.length === 1) {
return matrix[0];
}
const head = matrix[0];
const nested = expand(matrix.slice(1));
const combined = head.map((prefix) => nested.map((suffix) => prefix + suffix));
return ([] as string[]).concat(...combined);
}

function* extractLiterals(type: BaseType | undefined): Iterable<string> {
if (!type) return;
if (type instanceof LiteralType) {
yield type.getValue().toString();
return;
}
if (type instanceof UnionType) {
for (const t of type.getTypes()) {
yield* extractLiterals(t);
}
return;
}
if (type instanceof AliasType) {
yield* extractLiterals(type.getType());
return;
}

throw new UnknownTypeError(type);
}
2 changes: 2 additions & 0 deletions test/valid-data-other.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ describe("valid-data-other", () => {
it("string-literals", assertValidSchema("string-literals", "MyObject"));
it("string-literals-inline", assertValidSchema("string-literals-inline", "MyObject"));
it("string-literals-null", assertValidSchema("string-literals-null", "MyObject"));
it("string-template-literals", assertValidSchema("string-template-literals", "MyObject"));
it("string-template-expression-literals", assertValidSchema("string-template-expression-literals", "MyObject"));

it("namespace-deep-1", assertValidSchema("namespace-deep-1", "RootNamespace.Def"));
it("namespace-deep-2", assertValidSchema("namespace-deep-2", "RootNamespace.SubNamespace.HelperA"));
Expand Down
10 changes: 10 additions & 0 deletions test/valid-data/string-template-expression-literals/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
type OK = "ok";
type Result = OK | "fail" | `abort`;
type PrivateResultId = `__${Result}_id`;
type OK_ID = `id_${OK}`;

export interface MyObject {
foo: Result;
_foo: PrivateResultId;
ok: OK_ID;
}
37 changes: 37 additions & 0 deletions test/valid-data/string-template-expression-literals/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"$ref": "#/definitions/MyObject",
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"MyObject": {
"additionalProperties": false,
"properties": {
"_foo": {
"enum": [
"__ok_id",
"__fail_id",
"__abort_id"
],
"type": "string"
},
"foo": {
"enum": [
"ok",
"fail",
"abort"
],
"type": "string"
},
"ok": {
"const": "id_ok",
"type": "string"
}
},
"required": [
"foo",
"_foo",
"ok"
],
"type": "object"
}
}
}
5 changes: 5 additions & 0 deletions test/valid-data/string-template-literals/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type Result = "ok" | "fail" | `abort`;

export interface MyObject {
foo: Result;
}
23 changes: 23 additions & 0 deletions test/valid-data/string-template-literals/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$ref": "#/definitions/MyObject",
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"MyObject": {
"additionalProperties": false,
"properties": {
"foo": {
"enum": [
"ok",
"fail",
"abort"
],
"type": "string"
}
},
"required": [
"foo"
],
"type": "object"
}
}
}

0 comments on commit 642022c

Please sign in to comment.