Skip to content

Commit e3d1117

Browse files
committed
Improvements: module export, support for negative enum values, better support for generic inheritance
1 parent fe6aef0 commit e3d1117

8 files changed

Lines changed: 107 additions & 23 deletions

File tree

src/typesharp-ts/dist/main.js

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/typesharp-ts/dist/main.mjs

Lines changed: 8 additions & 0 deletions
Large diffs are not rendered by default.

src/typesharp-ts/esbuild.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ await esbuild.build({
99
platform: 'node',
1010
format: 'esm',
1111
entryPoints: [ './src/main.ts' ],
12-
outdir: './dist'
12+
outfile: './dist/main.mjs'
1313
});

src/typesharp-ts/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@onboxdesign/onbox-typesharp",
33
"version": "0.0.4",
44
"description": "Commandline app to convert CSharp data models into Typescript",
5-
"main": "./dist/main.js",
5+
"main": "./dist/main.mjs",
66
"types": "./dist/main.d.ts",
77
"type": "module",
88
"scripts": {

src/typesharp-ts/spec/node_test.ts

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import test from "node:test";
1+
import { test } from "vitest";
22
import { strict as assert } from "node:assert";
33
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
44
import { tmpdir } from "node:os";
@@ -72,6 +72,27 @@ namespace Demo {
7272
assert.match(person, /score: 1\.2 \| 2\.2;/);
7373
});
7474

75+
test("supports signed enum values", () => {
76+
const generated = new Map(generate([
77+
parseSourceFile(
78+
"status.cs",
79+
`
80+
public enum Status {
81+
Invalid = -1,
82+
None = 0,
83+
Valid = +1,
84+
Next
85+
}`,
86+
),
87+
]).map((file) => [file.name, file.text]));
88+
89+
const status = generated.get("Status.ts")!;
90+
assert.match(status, /Invalid = -1,/);
91+
assert.match(status, /None = 0,/);
92+
assert.match(status, /Valid = 1,/);
93+
assert.match(status, /Next = 2,/);
94+
});
95+
7596
test("supports configured Record dictionaries, readonly properties, quotes, and semicolons", () => {
7697
const generated = new Map(generate([
7798
parseSourceFile(
@@ -116,10 +137,52 @@ public class Person {
116137
assert.match(person, /name: string;/);
117138
});
118139

140+
test("supports interfaces and multiple implemented generic interfaces", () => {
141+
const generated = new Map(generate([
142+
parseSourceFile(
143+
"engineering.cs",
144+
`
145+
using System.Collections.Generic;
146+
147+
namespace Shedmate.Designer.Models.Engineering.FEM
148+
{
149+
public class EngineeringResult : IOptionalError<List<string>>, IOptionalWarning<List<string>>
150+
{
151+
public bool Done { get; set; }
152+
public bool Success { get; set ; }
153+
public List<string> Error { get; set; }
154+
public List<string> Warning { get; set; }
155+
public string Critical { get; set; }
156+
}
157+
158+
public interface IOptionalError<T>
159+
{
160+
public bool Success { get; set; }
161+
public T Error { get; set;}
162+
}
163+
164+
public interface IOptionalWarning<T>
165+
{
166+
public bool Success { get; set; }
167+
public T Warning { get; set;}
168+
}
169+
}`,
170+
),
171+
]).map((file) => [file.name, file.text]));
172+
173+
assert.match(
174+
generated.get("EngineeringResult.ts")!,
175+
/export interface EngineeringResult extends IOptionalError<string\[\]>, IOptionalWarning<string\[\]> \{/,
176+
);
177+
assert.match(generated.get("IOptionalError.ts")!, /export interface IOptionalError<T> \{/);
178+
assert.match(generated.get("IOptionalError.ts")!, /error: T;/);
179+
assert.match(generated.get("IOptionalWarning.ts")!, /warning: T;/);
180+
});
181+
119182
test("generates sample outputs for current golden DTOs", async () => {
120183
const temp = await mkdtemp(join(tmpdir(), "typesharp-"));
121184
try {
122-
const root = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
185+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
123186
await convert({
124187
source: join(root, "samples/SampleModels"),
125188
fileFilter: "*.cs",

src/typesharp-ts/src/ast.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface ClassNode {
3737
namespaceName?: string;
3838
typeParameters: string[];
3939
baseType?: TypeReferenceNode;
40+
baseTypes?: TypeReferenceNode[];
4041
properties: PropertyNode[];
4142
attributes: AttributeNode[];
4243
leadingComments: CommentTrivia[];

src/typesharp-ts/src/generator.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,10 @@ function generateClass(node: ClassNode, typeMap: TypeMap, options: Required<Gene
7878
}
7979
lines.push("");
8080
const typeParams = node.typeParameters.length > 0 ? `<${node.typeParameters.join(", ")}>` : "";
81-
const base = node.baseType ? ` extends ${toTypeScriptType(node.baseType, node, typeMap, options)}` : "";
81+
const baseTypes = node.baseTypes ?? (node.baseType ? [node.baseType] : []);
82+
const base = baseTypes.length > 0
83+
? ` extends ${baseTypes.map((type) => toTypeScriptType(type, node, typeMap, options)).join(", ")}`
84+
: "";
8285
const readonlyClass = options.readonlyProperties || hasAttribute(node.attributes, "Readonly") ||
8386
hasAttribute(node.attributes, "ReadOnly") || hasAttribute(node.attributes, "ReadonlyProperties");
8487
lines.push(`export interface ${node.name}${typeParams}${base} {`);
@@ -115,7 +118,7 @@ function collectImports(node: ClassNode, typeMap: TypeMap): string[] {
115118
}
116119
for (const arg of type.args) visit(arg);
117120
};
118-
if (node.baseType) visit(node.baseType);
121+
for (const baseType of node.baseTypes ?? (node.baseType ? [node.baseType] : [])) visit(baseType);
119122
for (const prop of node.properties) visit(prop.type);
120123
return [...imports];
121124
}

src/typesharp-ts/src/parser.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class Parser {
6565

6666
if (
6767
attributes.length > 0 || this.isText("public") || this.isText("partial") || this.isText("class") ||
68-
this.isText("enum")
68+
this.isText("interface") || this.isText("enum")
6969
) {
7070
declarations.push(...this.parseDeclaration(undefined, attributes));
7171
continue;
@@ -83,7 +83,11 @@ class Parser {
8383
this.matchText("partial");
8484

8585
if (this.matchText("class")) {
86-
return [this.parseClass(namespaceName, attributes, leadingComments)];
86+
return [this.parseClass(namespaceName, attributes, leadingComments, "class")];
87+
}
88+
89+
if (this.matchText("interface")) {
90+
return [this.parseClass(namespaceName, attributes, leadingComments, "interface")];
8791
}
8892

8993
if (this.matchText("enum")) {
@@ -99,11 +103,11 @@ class Parser {
99103
namespaceName: string | undefined,
100104
attributes: AttributeNode[],
101105
leadingComments: CommentTrivia[],
106+
declarationKind: "class" | "interface",
102107
): ClassNode {
103-
const name = this.expectIdentifier("Expected class name").text;
108+
const name = this.expectIdentifier(`Expected ${declarationKind} name`).text;
104109
const typeParameters = this.parseTypeParameters();
105-
let baseType: TypeReferenceNode | undefined;
106-
if (this.matchText(":")) baseType = this.parseTypeReference();
110+
const baseTypes = this.parseBaseTypes();
107111
this.expectText("{");
108112
const properties: PropertyNode[] = [];
109113

@@ -115,7 +119,7 @@ class Parser {
115119
continue;
116120
}
117121

118-
if (this.isText("class") || this.isText("enum")) {
122+
if (this.isText("class") || this.isText("interface") || this.isText("enum")) {
119123
this.skipUnsupportedMember("Nested type declarations are not supported");
120124
continue;
121125
}
@@ -150,14 +154,22 @@ class Parser {
150154
name,
151155
namespaceName,
152156
typeParameters,
153-
baseType,
157+
baseType: baseTypes[0],
158+
baseTypes,
154159
properties,
155160
attributes,
156161
leadingComments,
157162
trailingComments: this.previous().trailingComments,
158163
};
159164
}
160165

166+
private parseBaseTypes(): TypeReferenceNode[] {
167+
const baseTypes: TypeReferenceNode[] = [];
168+
if (!this.matchText(":")) return baseTypes;
169+
do baseTypes.push(this.parseTypeReference()); while (this.matchText(","));
170+
return baseTypes;
171+
}
172+
161173
private parseEnum(
162174
namespaceName: string | undefined,
163175
attributes: AttributeNode[],
@@ -172,8 +184,7 @@ class Parser {
172184
const token = this.expectIdentifier("Expected enum member name");
173185
let value: number | undefined;
174186
if (this.matchText("=")) {
175-
const valueToken = this.expect("number", "Expected enum member numeric value");
176-
value = Number(valueToken.text);
187+
value = this.parseEnumNumericValue();
177188
nextImplicitValue = value + 1;
178189
} else {
179190
value = nextImplicitValue++;
@@ -198,6 +209,12 @@ class Parser {
198209
};
199210
}
200211

212+
private parseEnumNumericValue(): number {
213+
const sign = this.matchText("-") ? -1 : this.matchText("+") ? 1 : 1;
214+
const valueToken = this.expect("number", "Expected enum member numeric value");
215+
return sign * Number(valueToken.text);
216+
}
217+
201218
private parseAttributes(): AttributeNode[] {
202219
const attributes: AttributeNode[] = [];
203220
while (this.matchText("[")) {

0 commit comments

Comments
 (0)