Skip to content

Commit

Permalink
feat: support property defaults with constant and enum refs (#336)
Browse files Browse the repository at this point in the history
* add helper functions getDeclarationValue() and getInitializerValue()
* make user symbols available to sandbox evaluation
* simplify initializer logic (eliminate ifs)
  • Loading branch information
ahochsteger committed May 2, 2024
1 parent 3426762 commit 5ae6704
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 23 deletions.
26 changes: 26 additions & 0 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions test/programs/default-properties-ref/main.ts
Original file line number Diff line number Diff line change
@@ -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;
}
48 changes: 48 additions & 0 deletions test/programs/default-properties-ref/schema.json
Original file line number Diff line number Diff line change
@@ -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"
}
1 change: 1 addition & 0 deletions test/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
95 changes: 72 additions & 23 deletions typescript-json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -847,31 +896,30 @@ export class JsonSchemaGenerator {
initial = initial.expression;
}

if ((<any>initial).expression) {
// node
console.warn("initializer is expression for property " + propertyName);
} else if ((<any>initial).kind && (<any>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<string, any> = { 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);
}
}

Expand Down Expand Up @@ -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 ||
Expand Down

0 comments on commit 5ae6704

Please sign in to comment.