Skip to content

Commit 5ae6704

Browse files
committed
feat: support property defaults with constant and enum refs (YousefED#336)
* add helper functions getDeclarationValue() and getInitializerValue() * make user symbols available to sandbox evaluation * simplify initializer logic (eliminate ifs)
1 parent 3426762 commit 5ae6704

File tree

5 files changed

+167
-23
lines changed

5 files changed

+167
-23
lines changed

api.md

+26
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,32 @@ class MyObject {
667667
```
668668

669669

670+
## [default-properties-ref](./test/programs/default-properties-ref)
671+
672+
```ts
673+
674+
const defaultBooleanFalse = false;
675+
const defaultBooleanTrue = true;
676+
const defaultFloat = 12.3;
677+
const defaultInteger = 123;
678+
const defaultString = "test"
679+
680+
enum FruitEnum {
681+
Apple = 'apple',
682+
Orange = 'orange'
683+
}
684+
685+
class MyObject {
686+
propBooleanFalse: boolean = defaultBooleanFalse;
687+
propBooleanTrue: boolean = defaultBooleanTrue;
688+
propFloat: number = defaultFloat;
689+
propInteger: number = defaultInteger;
690+
propString: string = defaultString;
691+
propEnum: FruitEnum = FruitEnum.Apple;
692+
}
693+
```
694+
695+
670696
## [enums-compiled-compute](./test/programs/enums-compiled-compute)
671697

672698
```ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
2+
const defaultBooleanFalse = false;
3+
const defaultBooleanTrue = true;
4+
const defaultFloat = 12.3;
5+
const defaultInteger = 123;
6+
const defaultString = "test"
7+
8+
enum FruitEnum {
9+
Apple = 'apple',
10+
Orange = 'orange'
11+
}
12+
13+
class MyObject {
14+
propBooleanFalse: boolean = defaultBooleanFalse;
15+
propBooleanTrue: boolean = defaultBooleanTrue;
16+
propFloat: number = defaultFloat;
17+
propInteger: number = defaultInteger;
18+
propString: string = defaultString;
19+
propEnum: FruitEnum = FruitEnum.Apple;
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"additionalProperties": false,
4+
"definitions": {
5+
"FruitEnum": {
6+
"enum": [
7+
"apple",
8+
"orange"
9+
],
10+
"type": "string"
11+
}
12+
},
13+
"properties": {
14+
"propBooleanFalse": {
15+
"type": "boolean",
16+
"default": false
17+
},
18+
"propBooleanTrue": {
19+
"type": "boolean",
20+
"default": true
21+
},
22+
"propEnum": {
23+
"$ref": "#/definitions/FruitEnum",
24+
"default": "apple"
25+
},
26+
"propFloat": {
27+
"type": "number",
28+
"default": 12.3
29+
},
30+
"propInteger": {
31+
"type": "number",
32+
"default": 123
33+
},
34+
"propString": {
35+
"type": "string",
36+
"default": "test"
37+
}
38+
},
39+
"required": [
40+
"propBooleanFalse",
41+
"propBooleanTrue",
42+
"propEnum",
43+
"propFloat",
44+
"propInteger",
45+
"propString"
46+
],
47+
"type": "object"
48+
}

test/schema.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ describe("schema", () => {
411411
assertSchema("ignored-required", "MyObject");
412412

413413
assertSchema("default-properties", "MyObject");
414+
assertSchema("default-properties-ref", "MyObject");
414415

415416
// not supported yet #116
416417
// assertSchema("interface-extra-props", "MyObject");

typescript-json-schema.ts

+72-23
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,55 @@ export class JsonSchemaGenerator {
819819
return undefined;
820820
}
821821

822+
private getInitializerValue(initializer?: ts.Expression | ts.LiteralToken): any {
823+
let val;
824+
if (initializer === undefined) {
825+
return;
826+
}
827+
switch (initializer.kind) {
828+
case ts.SyntaxKind.NumericLiteral:
829+
const txt = initializer.getText();
830+
if (txt.includes(".")) {
831+
val = Number.parseFloat(initializer.getText());
832+
} else {
833+
val = Number.parseInt(initializer.getText());
834+
}
835+
break;
836+
case ts.SyntaxKind.StringLiteral:
837+
val = (initializer as ts.StringLiteral).text;
838+
break;
839+
case ts.SyntaxKind.FalseKeyword:
840+
val = false;
841+
break;
842+
case ts.SyntaxKind.TrueKeyword:
843+
val = true;
844+
break;
845+
}
846+
return val;
847+
}
848+
849+
private getDeclarationValue(declaration?: ts.Declaration): any {
850+
let val: any;
851+
if (declaration === undefined) {
852+
return;
853+
}
854+
switch (declaration.kind) {
855+
case ts.SyntaxKind.VariableDeclaration:
856+
val = this.getInitializerValue((declaration as ts.VariableDeclaration).initializer);
857+
break;
858+
case ts.SyntaxKind.EnumDeclaration:
859+
const enumDecl = declaration as ts.EnumDeclaration;
860+
val = enumDecl.members.reduce((prev, curr) => {
861+
const v = this.getInitializerValue(curr.initializer);
862+
prev[curr.name.getText()] = v;
863+
return prev;
864+
}, {} as { [k: string]: any });
865+
866+
break;
867+
}
868+
return val;
869+
}
870+
822871
private getDefinitionForProperty(prop: ts.Symbol, node: ts.Node): Definition | null {
823872
if (prop.flags & ts.SymbolFlags.Method) {
824873
return null;
@@ -847,31 +896,30 @@ export class JsonSchemaGenerator {
847896
initial = initial.expression;
848897
}
849898

850-
if ((<any>initial).expression) {
851-
// node
852-
console.warn("initializer is expression for property " + propertyName);
853-
} else if ((<any>initial).kind && (<any>initial).kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral) {
854-
definition.default = initial.getText();
855-
} else {
856-
try {
857-
const sandbox = { sandboxvar: null as any };
858-
vm.runInNewContext("sandboxvar=" + initial.getText(), sandbox);
899+
try {
900+
const sandbox: Record<string, any> = { sandboxvar: null as any };
901+
// Put user symbols into sandbox
902+
Object.entries(this.userSymbols)
903+
.filter(([_, sym]) => sym.valueDeclaration)
904+
.forEach(([name, sym]) => {
905+
sandbox[name] = this.getDeclarationValue(sym.valueDeclaration);
906+
});
907+
vm.runInNewContext("sandboxvar=" + initial.getText(), sandbox);
859908

860-
const val = sandbox.sandboxvar;
861-
if (
862-
val === null ||
863-
typeof val === "string" ||
864-
typeof val === "number" ||
865-
typeof val === "boolean" ||
866-
Object.prototype.toString.call(val) === "[object Array]"
867-
) {
868-
definition.default = val;
869-
} else if (val) {
870-
console.warn("unknown initializer for property " + propertyName + ": " + val);
871-
}
872-
} catch (e) {
873-
console.warn("exception evaluating initializer for property " + propertyName);
909+
const val = sandbox.sandboxvar;
910+
if (
911+
val === null ||
912+
typeof val === "string" ||
913+
typeof val === "number" ||
914+
typeof val === "boolean" ||
915+
Object.prototype.toString.call(val) === "[object Array]"
916+
) {
917+
definition.default = val;
918+
} else if (val) {
919+
console.warn("unknown initializer for property " + propertyName + ": " + val);
874920
}
921+
} catch (e) {
922+
console.warn("exception evaluating initializer for property " + propertyName);
875923
}
876924
}
877925

@@ -1700,6 +1748,7 @@ export function buildGenerator(
17001748

17011749
function inspect(node: ts.Node, tc: ts.TypeChecker) {
17021750
if (
1751+
node.kind === ts.SyntaxKind.VariableDeclaration ||
17031752
node.kind === ts.SyntaxKind.ClassDeclaration ||
17041753
node.kind === ts.SyntaxKind.InterfaceDeclaration ||
17051754
node.kind === ts.SyntaxKind.EnumDeclaration ||

0 commit comments

Comments
 (0)