diff --git a/factory/parser.ts b/factory/parser.ts index f93331a75..b22a1172f 100644 --- a/factory/parser.ts +++ b/factory/parser.ts @@ -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"; @@ -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()) diff --git a/src/NodeParser/StringTemplateLiteralNodeParser.ts b/src/NodeParser/StringTemplateLiteralNodeParser.ts new file mode 100644 index 000000000..4ef75f053 --- /dev/null +++ b/src/NodeParser/StringTemplateLiteralNodeParser.ts @@ -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 { + 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); +} diff --git a/test/valid-data-other.test.ts b/test/valid-data-other.test.ts index 833df917d..70239a846 100644 --- a/test/valid-data-other.test.ts +++ b/test/valid-data-other.test.ts @@ -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")); diff --git a/test/valid-data/string-template-expression-literals/main.ts b/test/valid-data/string-template-expression-literals/main.ts new file mode 100644 index 000000000..4209a85d3 --- /dev/null +++ b/test/valid-data/string-template-expression-literals/main.ts @@ -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; +} diff --git a/test/valid-data/string-template-expression-literals/schema.json b/test/valid-data/string-template-expression-literals/schema.json new file mode 100644 index 000000000..41d331ab8 --- /dev/null +++ b/test/valid-data/string-template-expression-literals/schema.json @@ -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" + } + } +} diff --git a/test/valid-data/string-template-literals/main.ts b/test/valid-data/string-template-literals/main.ts new file mode 100644 index 000000000..6739d36a1 --- /dev/null +++ b/test/valid-data/string-template-literals/main.ts @@ -0,0 +1,5 @@ +type Result = "ok" | "fail" | `abort`; + +export interface MyObject { + foo: Result; +} diff --git a/test/valid-data/string-template-literals/schema.json b/test/valid-data/string-template-literals/schema.json new file mode 100644 index 000000000..f55afb48b --- /dev/null +++ b/test/valid-data/string-template-literals/schema.json @@ -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" + } + } +}