Skip to content

Commit

Permalink
feat: support intrinsic string manipulation types (#1173)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason3S committed Mar 21, 2022
1 parent 642022c commit 8f2dc15
Show file tree
Hide file tree
Showing 11 changed files with 216 additions and 23 deletions.
4 changes: 4 additions & 0 deletions .editorconfig
Expand Up @@ -14,6 +14,10 @@ indent_size = 4
indent_style = space
indent_size = 4

[test/**/schema.json]
indent_style = space
indent_size = 2

[{package.json,azure-pipelines.yml}]
indent_style = space
indent_size = 2
27 changes: 27 additions & 0 deletions .vscode/launch.json
Expand Up @@ -4,6 +4,33 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
//
// Helps with debugging parsing.
// - Set break points in the code
// - Open the file you want to parse: test/**/main.ts
// - F5 to run the debugger.
"name": "Debug Test Case",
"type": "node",
"request": "launch",
"runtimeExecutable": "node",
"runtimeArgs": [
"--nolazy",
"-r",
"ts-node/register"
],
"args": [
"ts-json-schema-generator.ts",
"-p",
"${file}"
],
"cwd": "${workspaceFolder}",
"internalConsoleOptions": "openOnSessionStart",
"skipFiles": [
"<node_internals>/**",
"node_modules/**"
]
},
{
"type": "node",
"request": "attach",
Expand Down
2 changes: 2 additions & 0 deletions factory/parser.ts
Expand Up @@ -24,6 +24,7 @@ import { HiddenNodeParser } from "../src/NodeParser/HiddenTypeNodeParser";
import { IndexedAccessTypeNodeParser } from "../src/NodeParser/IndexedAccessTypeNodeParser";
import { InterfaceAndClassNodeParser } from "../src/NodeParser/InterfaceAndClassNodeParser";
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 { NeverTypeNodeParser } from "../src/NodeParser/NeverTypeNodeParser";
Expand Down Expand Up @@ -104,6 +105,7 @@ export function createParser(program: ts.Program, config: Config, augmentor?: Pa
.addNodeParser(withJsDoc(new ParameterParser(chainNodeParser)))
.addNodeParser(new StringLiteralNodeParser())
.addNodeParser(new StringTemplateLiteralNodeParser(chainNodeParser))
.addNodeParser(new IntrinsicNodeParser())
.addNodeParser(new NumberLiteralNodeParser())
.addNodeParser(new BooleanLiteralNodeParser())
.addNodeParser(new NullLiteralNodeParser())
Expand Down
39 changes: 39 additions & 0 deletions src/NodeParser/IntrinsicNodeParser.ts
@@ -0,0 +1,39 @@
import ts from "typescript";
import { Context } from "../NodeParser";
import { SubNodeParser } from "../SubNodeParser";
import { BaseType } from "../Type/BaseType";
import { LiteralType } from "../Type/LiteralType";
import { UnionType } from "../Type/UnionType";
import assert from "../Utils/assert";
import { extractLiterals } from "../Utils/extractLiterals";

export const intrinsicMethods: Record<string, ((v: string) => string) | undefined> = {
Uppercase: (v) => v.toUpperCase(),
Lowercase: (v) => v.toLowerCase(),
Capitalize: (v) => v[0].toUpperCase() + v.slice(1),
Uncapitalize: (v) => v[0].toLowerCase() + v.slice(1),
};

export class IntrinsicNodeParser implements SubNodeParser {
public supportsNode(node: ts.KeywordTypeNode): boolean {
return node.kind === ts.SyntaxKind.IntrinsicKeyword;
}
public createType(node: ts.KeywordTypeNode, context: Context): BaseType | undefined {
const methodName = getParentName(node);
const method = intrinsicMethods[methodName];
assert(method, `Unknown intrinsic method: ${methodName}`);
const literals = extractLiterals(context.getArguments()[0])
.map(method)
.map((literal) => new LiteralType(literal));
if (literals.length === 1) {
return literals[0];
}
return new UnionType(literals);
}
}

function getParentName(node: ts.KeywordTypeNode): string {
const parent = node.parent;
assert(ts.isTypeAliasDeclaration(parent), "Only intrinsics part of a TypeAliasDeclaration are supported.");
return parent.name.text;
}
25 changes: 2 additions & 23 deletions src/NodeParser/StringTemplateLiteralNodeParser.ts
@@ -1,11 +1,10 @@
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";
import { extractLiterals } from "../Utils/extractLiterals";

export class StringTemplateLiteralNodeParser implements SubNodeParser {
public constructor(protected childNodeParser: NodeParser) {}
Expand All @@ -24,7 +23,7 @@ export class StringTemplateLiteralNodeParser implements SubNodeParser {
node.templateSpans.map((span) => {
const suffix = span.literal.text;
const type = this.childNodeParser.createType(span.type, context);
return [...extractLiterals(type)].map((value) => value + suffix);
return extractLiterals(type).map((value) => value + suffix);
})
);

Expand All @@ -49,23 +48,3 @@ function expand(matrix: string[][]): string[] {
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);
}
7 changes: 7 additions & 0 deletions src/Utils/assert.ts
@@ -0,0 +1,7 @@
import { LogicError } from "../Error/LogicError";

export default function assert(value: unknown, message: string): asserts value {
if (!value) {
throw new LogicError(message);
}
}
31 changes: 31 additions & 0 deletions src/Utils/extractLiterals.ts
@@ -0,0 +1,31 @@
import { UnknownTypeError } from "../Error/UnknownTypeError";
import { AliasType } from "../Type/AliasType";
import { BaseType } from "../Type/BaseType";
import { LiteralType } from "../Type/LiteralType";
import { UnionType } from "../Type/UnionType";

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);
}

export function extractLiterals(type: BaseType | undefined): string[] {
return [..._extractLiterals(type)];
}
25 changes: 25 additions & 0 deletions test/unit/assert.test.ts
@@ -0,0 +1,25 @@
import { LogicError } from "../../src/Error/LogicError";
import assert from "../../src/Utils/assert";

describe("validate assert", () => {
it.each`
value
${"hello"}
${1}
${true}
${{}}
`("success $value", ({ value }) => {
expect(() => assert(value, "message")).not.toThrow();
});

it.each`
value
${""}
${0}
${false}
${undefined}
${null}
`("fail $value", ({ value }) => {
expect(() => assert(value, "failed to be true")).toThrowError(LogicError);
});
});
1 change: 1 addition & 0 deletions test/valid-data-other.test.ts
Expand Up @@ -36,6 +36,7 @@ describe("valid-data-other", () => {

it("string-literals", assertValidSchema("string-literals", "MyObject"));
it("string-literals-inline", assertValidSchema("string-literals-inline", "MyObject"));
it("string-literals-intrinsic", assertValidSchema("string-literals-intrinsic", "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"));
Expand Down
14 changes: 14 additions & 0 deletions test/valid-data/string-literals-intrinsic/main.ts
@@ -0,0 +1,14 @@
type Abort = "abort";
type Result = "ok" | "fail" | Uppercase<Abort> | "Success";
type ResultUpper = Uppercase<Result>;
type ResultLower = Lowercase<ResultUpper>;
type ResultCapitalize = Capitalize<Result>;
type ResultUncapitalize = Uncapitalize<ResultCapitalize>;

export interface MyObject {
result: Result;
resultUpper: ResultUpper;
resultLower: ResultLower;
resultCapitalize: ResultCapitalize;
resultUncapitalize: ResultUncapitalize;
}
64 changes: 64 additions & 0 deletions test/valid-data/string-literals-intrinsic/schema.json
@@ -0,0 +1,64 @@
{
"$ref": "#/definitions/MyObject",
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"MyObject": {
"additionalProperties": false,
"properties": {
"result": {
"enum": [
"ok",
"fail",
"ABORT",
"Success"
],
"type": "string"
},
"resultCapitalize": {
"enum": [
"Ok",
"Fail",
"ABORT",
"Success"
],
"type": "string"
},
"resultLower": {
"enum": [
"ok",
"fail",
"abort",
"success"
],
"type": "string"
},
"resultUncapitalize": {
"enum": [
"ok",
"fail",
"aBORT",
"success"
],
"type": "string"
},
"resultUpper": {
"enum": [
"OK",
"FAIL",
"ABORT",
"SUCCESS"
],
"type": "string"
}
},
"required": [
"result",
"resultUpper",
"resultLower",
"resultCapitalize",
"resultUncapitalize"
],
"type": "object"
}
}
}

0 comments on commit 8f2dc15

Please sign in to comment.