From 5ae670448fe2b957507b86f11066eb04bc4a54f8 Mon Sep 17 00:00:00 2001 From: Andreas Hochsteger Date: Thu, 2 May 2024 06:53:23 +0200 Subject: [PATCH] feat: support property defaults with constant and enum refs (#336) * add helper functions getDeclarationValue() and getInitializerValue() * make user symbols available to sandbox evaluation * simplify initializer logic (eliminate ifs) --- api.md | 26 +++++ test/programs/default-properties-ref/main.ts | 20 ++++ .../default-properties-ref/schema.json | 48 ++++++++++ test/schema.test.ts | 1 + typescript-json-schema.ts | 95 ++++++++++++++----- 5 files changed, 167 insertions(+), 23 deletions(-) create mode 100644 test/programs/default-properties-ref/main.ts create mode 100644 test/programs/default-properties-ref/schema.json diff --git a/api.md b/api.md index bb47b106..2d835df5 100644 --- a/api.md +++ b/api.md @@ -667,6 +667,32 @@ class MyObject { ``` +## [default-properties-ref](./test/programs/default-properties-ref) + +```ts + +const defaultBooleanFalse = false; +const defaultBooleanTrue = true; +const defaultFloat = 12.3; +const defaultInteger = 123; +const defaultString = "test" + +enum FruitEnum { + Apple = 'apple', + Orange = 'orange' +} + +class MyObject { + propBooleanFalse: boolean = defaultBooleanFalse; + propBooleanTrue: boolean = defaultBooleanTrue; + propFloat: number = defaultFloat; + propInteger: number = defaultInteger; + propString: string = defaultString; + propEnum: FruitEnum = FruitEnum.Apple; +} +``` + + ## [enums-compiled-compute](./test/programs/enums-compiled-compute) ```ts diff --git a/test/programs/default-properties-ref/main.ts b/test/programs/default-properties-ref/main.ts new file mode 100644 index 00000000..af88d2c9 --- /dev/null +++ b/test/programs/default-properties-ref/main.ts @@ -0,0 +1,20 @@ + +const defaultBooleanFalse = false; +const defaultBooleanTrue = true; +const defaultFloat = 12.3; +const defaultInteger = 123; +const defaultString = "test" + +enum FruitEnum { + Apple = 'apple', + Orange = 'orange' +} + +class MyObject { + propBooleanFalse: boolean = defaultBooleanFalse; + propBooleanTrue: boolean = defaultBooleanTrue; + propFloat: number = defaultFloat; + propInteger: number = defaultInteger; + propString: string = defaultString; + propEnum: FruitEnum = FruitEnum.Apple; +} diff --git a/test/programs/default-properties-ref/schema.json b/test/programs/default-properties-ref/schema.json new file mode 100644 index 00000000..71f6bf5c --- /dev/null +++ b/test/programs/default-properties-ref/schema.json @@ -0,0 +1,48 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "FruitEnum": { + "enum": [ + "apple", + "orange" + ], + "type": "string" + } + }, + "properties": { + "propBooleanFalse": { + "type": "boolean", + "default": false + }, + "propBooleanTrue": { + "type": "boolean", + "default": true + }, + "propEnum": { + "$ref": "#/definitions/FruitEnum", + "default": "apple" + }, + "propFloat": { + "type": "number", + "default": 12.3 + }, + "propInteger": { + "type": "number", + "default": 123 + }, + "propString": { + "type": "string", + "default": "test" + } + }, + "required": [ + "propBooleanFalse", + "propBooleanTrue", + "propEnum", + "propFloat", + "propInteger", + "propString" + ], + "type": "object" +} diff --git a/test/schema.test.ts b/test/schema.test.ts index a29fe972..b0ae18c7 100644 --- a/test/schema.test.ts +++ b/test/schema.test.ts @@ -411,6 +411,7 @@ describe("schema", () => { assertSchema("ignored-required", "MyObject"); assertSchema("default-properties", "MyObject"); + assertSchema("default-properties-ref", "MyObject"); // not supported yet #116 // assertSchema("interface-extra-props", "MyObject"); diff --git a/typescript-json-schema.ts b/typescript-json-schema.ts index 68c34bf2..ad195c5e 100644 --- a/typescript-json-schema.ts +++ b/typescript-json-schema.ts @@ -819,6 +819,55 @@ export class JsonSchemaGenerator { return undefined; } + private getInitializerValue(initializer?: ts.Expression | ts.LiteralToken): any { + let val; + if (initializer === undefined) { + return; + } + switch (initializer.kind) { + case ts.SyntaxKind.NumericLiteral: + const txt = initializer.getText(); + if (txt.includes(".")) { + val = Number.parseFloat(initializer.getText()); + } else { + val = Number.parseInt(initializer.getText()); + } + break; + case ts.SyntaxKind.StringLiteral: + val = (initializer as ts.StringLiteral).text; + break; + case ts.SyntaxKind.FalseKeyword: + val = false; + break; + case ts.SyntaxKind.TrueKeyword: + val = true; + break; + } + return val; + } + + private getDeclarationValue(declaration?: ts.Declaration): any { + let val: any; + if (declaration === undefined) { + return; + } + switch (declaration.kind) { + case ts.SyntaxKind.VariableDeclaration: + val = this.getInitializerValue((declaration as ts.VariableDeclaration).initializer); + break; + case ts.SyntaxKind.EnumDeclaration: + const enumDecl = declaration as ts.EnumDeclaration; + val = enumDecl.members.reduce((prev, curr) => { + const v = this.getInitializerValue(curr.initializer); + prev[curr.name.getText()] = v; + return prev; + }, {} as { [k: string]: any }); + + break; + } + return val; + } + private getDefinitionForProperty(prop: ts.Symbol, node: ts.Node): Definition | null { if (prop.flags & ts.SymbolFlags.Method) { return null; @@ -847,31 +896,30 @@ export class JsonSchemaGenerator { initial = initial.expression; } - if ((initial).expression) { - // node - console.warn("initializer is expression for property " + propertyName); - } else if ((initial).kind && (initial).kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral) { - definition.default = initial.getText(); - } else { - try { - const sandbox = { sandboxvar: null as any }; - vm.runInNewContext("sandboxvar=" + initial.getText(), sandbox); + try { + const sandbox: Record = { sandboxvar: null as any }; + // Put user symbols into sandbox + Object.entries(this.userSymbols) + .filter(([_, sym]) => sym.valueDeclaration) + .forEach(([name, sym]) => { + sandbox[name] = this.getDeclarationValue(sym.valueDeclaration); + }); + vm.runInNewContext("sandboxvar=" + initial.getText(), sandbox); - const val = sandbox.sandboxvar; - if ( - val === null || - typeof val === "string" || - typeof val === "number" || - typeof val === "boolean" || - Object.prototype.toString.call(val) === "[object Array]" - ) { - definition.default = val; - } else if (val) { - console.warn("unknown initializer for property " + propertyName + ": " + val); - } - } catch (e) { - console.warn("exception evaluating initializer for property " + propertyName); + const val = sandbox.sandboxvar; + if ( + val === null || + typeof val === "string" || + typeof val === "number" || + typeof val === "boolean" || + Object.prototype.toString.call(val) === "[object Array]" + ) { + definition.default = val; + } else if (val) { + console.warn("unknown initializer for property " + propertyName + ": " + val); } + } catch (e) { + console.warn("exception evaluating initializer for property " + propertyName); } } @@ -1700,6 +1748,7 @@ export function buildGenerator( function inspect(node: ts.Node, tc: ts.TypeChecker) { if ( + node.kind === ts.SyntaxKind.VariableDeclaration || node.kind === ts.SyntaxKind.ClassDeclaration || node.kind === ts.SyntaxKind.InterfaceDeclaration || node.kind === ts.SyntaxKind.EnumDeclaration ||