ajsc 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/JSONSchemaConverter.js +370 -0
  2. package/dist/JSONSchemaConverter.js.map +1 -0
  3. package/dist/JSONSchemaConverter.test.js +302 -0
  4. package/dist/JSONSchemaConverter.test.js.map +1 -0
  5. package/dist/TypescriptBaseConverter.js +131 -0
  6. package/dist/TypescriptBaseConverter.js.map +1 -0
  7. package/dist/TypescriptConverter.js +107 -0
  8. package/dist/TypescriptConverter.js.map +1 -0
  9. package/dist/TypescriptConverter.test.js +199 -0
  10. package/dist/TypescriptConverter.test.js.map +1 -0
  11. package/dist/TypescriptProcedureConverter.js +118 -0
  12. package/dist/TypescriptProcedureConverter.js.map +1 -0
  13. package/dist/TypescriptProceduresConverter.test.js +948 -0
  14. package/dist/TypescriptProceduresConverter.test.js.map +1 -0
  15. package/dist/types.js +2 -0
  16. package/dist/types.js.map +1 -0
  17. package/dist/utils/path-utils.js +78 -0
  18. package/dist/utils/path-utils.js.map +1 -0
  19. package/dist/utils/path-utils.test.js +92 -0
  20. package/dist/utils/path-utils.test.js.map +1 -0
  21. package/dist/utils/to-pascal-case.js +11 -0
  22. package/dist/utils/to-pascal-case.js.map +1 -0
  23. package/package.json +56 -0
  24. package/src/JSONSchemaConverter.test.ts +342 -0
  25. package/src/JSONSchemaConverter.ts +459 -0
  26. package/src/TypescriptBaseConverter.ts +161 -0
  27. package/src/TypescriptConverter.test.ts +264 -0
  28. package/src/TypescriptConverter.ts +161 -0
  29. package/src/TypescriptProcedureConverter.ts +160 -0
  30. package/src/TypescriptProceduresConverter.test.ts +952 -0
  31. package/src/types.ts +101 -0
  32. package/src/utils/path-utils.test.ts +102 -0
  33. package/src/utils/path-utils.ts +89 -0
  34. package/src/utils/to-pascal-case.ts +10 -0
@@ -0,0 +1,264 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { TypescriptConverter } from "./TypescriptConverter.js";
3
+
4
+ describe("TypeScriptLanguagePlugin", () => {
5
+ it("should convert strings", () => {
6
+ expect(new TypescriptConverter({ type: "string" }).code).toMatch("string");
7
+ });
8
+
9
+ it("should convert numbers", () => {
10
+ expect(new TypescriptConverter({ type: "number" }).code).toMatch("number");
11
+ });
12
+
13
+ it("should convert booleans", () => {
14
+ expect(new TypescriptConverter({ type: "boolean" }).code).toMatch(
15
+ "boolean",
16
+ );
17
+ });
18
+
19
+ it("should convert null", () => {
20
+ expect(new TypescriptConverter({ type: "null" }).code).toMatch("null");
21
+ });
22
+
23
+ it("should convert literals", () => {
24
+ expect(new TypescriptConverter({ const: "fixedValue" }).code).toMatch(
25
+ "fixedValue",
26
+ );
27
+ });
28
+
29
+ it("should convert enums", () => {
30
+ expect(
31
+ new TypescriptConverter({
32
+ type: "string",
33
+ enum: ["value1", "value2", "value3"],
34
+ }).code,
35
+ ).toMatch(`"value1" | "value2" | "value3"`);
36
+ });
37
+
38
+ it("should convert unions", () => {
39
+ expect(
40
+ new TypescriptConverter({ type: ["string", "number"] }).code,
41
+ ).toMatch(`string | number`);
42
+ });
43
+
44
+ it("should convert intersections", () => {
45
+ expect(
46
+ new TypescriptConverter({
47
+ allOf: [{ type: "string" }, { type: "number" }],
48
+ }).code,
49
+ ).toMatch(`string & number`);
50
+ });
51
+
52
+ it("should convert arrays", () => {
53
+ expect(
54
+ new TypescriptConverter({
55
+ type: "array",
56
+ items: { type: "string" },
57
+ }).code,
58
+ ).toMatch(`Array<string>`);
59
+ });
60
+
61
+ it("should convert objects", () => {
62
+ expect(
63
+ new TypescriptConverter({
64
+ type: "object",
65
+ properties: {
66
+ title: { type: "string" },
67
+ year: { type: "number" },
68
+ },
69
+ required: ["title"],
70
+ }).code,
71
+ ).toMatch(`{ title: string; year?: number; }`);
72
+ });
73
+
74
+ it("should convert an array of objects", () => {
75
+ expect(
76
+ new TypescriptConverter(
77
+ {
78
+ type: "array",
79
+ items: {
80
+ anyOf: [
81
+ {
82
+ type: "object",
83
+ properties: {
84
+ year: { type: "number" },
85
+ },
86
+ required: ["year"],
87
+ },
88
+ {
89
+ type: "object",
90
+ properties: {
91
+ title: { type: "string" },
92
+ },
93
+ },
94
+ ],
95
+ },
96
+ },
97
+ {
98
+ inlineTypes: true,
99
+ },
100
+ ).code,
101
+ ).toMatch(`Array<{ year: number; } | { title?: string; }>`);
102
+ });
103
+
104
+ it("should convert a simple JSON schema to Typescript", () => {
105
+ expect(
106
+ new TypescriptConverter(
107
+ {
108
+ type: "object",
109
+ properties: {
110
+ name: { type: "string" },
111
+ age: { type: "number" },
112
+ contacts: {
113
+ type: "array",
114
+ items: {
115
+ type: "object",
116
+ properties: {
117
+ email: { type: "string" },
118
+ },
119
+ required: ["email"],
120
+ },
121
+ },
122
+ profile: {
123
+ type: "object",
124
+ properties: {
125
+ email: { type: "string" },
126
+ },
127
+ required: ["email"],
128
+ },
129
+ },
130
+ required: ["name", "age"],
131
+ },
132
+ {
133
+ inlineTypes: true,
134
+ },
135
+ ).code.replace(/\s/g, ""),
136
+ ).toMatch(
137
+ `{
138
+ name: string;
139
+ age: number;
140
+ contacts?: Array<{ email: string; }>;
141
+ profile?: { email: string; };
142
+ }`.replace(/\s/g, ""),
143
+ );
144
+ });
145
+
146
+ it("should convert a named JSON schema top-level object to Typescript", () => {
147
+ expect(
148
+ new TypescriptConverter(
149
+ {
150
+ $schema: "http://json-schema.org/draft-07/schema#",
151
+ title: "Person",
152
+ type: "object",
153
+ properties: {
154
+ name: { type: "string" },
155
+ age: { type: "number" },
156
+ contacts: {
157
+ type: "array",
158
+ items: {
159
+ type: "object",
160
+ properties: {
161
+ email: { type: "string" },
162
+ },
163
+ required: ["email"],
164
+ },
165
+ },
166
+ profile: {
167
+ type: "object",
168
+ properties: {
169
+ email: { type: "string" },
170
+ },
171
+ required: ["email"],
172
+ },
173
+ },
174
+ required: ["name", "age"],
175
+ },
176
+ {
177
+ inlineTypes: true,
178
+ },
179
+ ).code.replace(/\s/g, ""),
180
+ ).toEqual(
181
+ `{ name: string; age: number; contacts?: Array<{ email: string; }>; profile?: { email: string; }; }`.replace(
182
+ /\s/g,
183
+ "",
184
+ ),
185
+ );
186
+ });
187
+
188
+ it("should convert a named JSON schema top-level array to Typescript", () => {
189
+ expect(
190
+ new TypescriptConverter(
191
+ {
192
+ $schema: "http://json-schema.org/draft-07/schema#",
193
+ title: "People",
194
+ type: "array",
195
+ items: {
196
+ type: "object",
197
+ properties: {
198
+ name: { type: "string" },
199
+ age: { type: "number" },
200
+ },
201
+ required: ["name", "age"],
202
+ },
203
+ },
204
+ {
205
+ inlineTypes: true,
206
+ },
207
+ ).code.replace(/\s/g, ""),
208
+ ).toEqual(`Array<{ name: string; age: number; }>`.replace(/\s/g, ""));
209
+ });
210
+
211
+ it("should convert JSON schema with top-level re-used objects in Typescript", async () => {
212
+ expect(
213
+ new TypescriptConverter(
214
+ {
215
+ $schema: "http://json-schema.org/draft-07/schema#",
216
+ title: "Person",
217
+ type: "object",
218
+ properties: {
219
+ contacts: {
220
+ type: "array",
221
+ items: {
222
+ type: "object",
223
+ properties: {
224
+ email: { type: "string" },
225
+ },
226
+ required: ["email"],
227
+ },
228
+ },
229
+ profile: {
230
+ type: "object",
231
+ properties: {
232
+ email: { type: "string" },
233
+ },
234
+ required: ["email"],
235
+ },
236
+ contact: {
237
+ type: "object",
238
+ properties: {
239
+ email: { type: "string" },
240
+ },
241
+ required: ["email"],
242
+ },
243
+ email: {
244
+ type: "object",
245
+ properties: {
246
+ email: { type: "string" },
247
+ },
248
+ required: ["email"],
249
+ },
250
+ },
251
+ required: ["name", "age"],
252
+ },
253
+ {
254
+ inlineTypes: true,
255
+ },
256
+ ).code.replace(/\s/g, ""),
257
+ ).toEqual(
258
+ `{contacts?:Array<{email:string;}>;profile?:{email:string;};contact?:{email:string;};email?:{email:string;};}`.replace(
259
+ /\s/g,
260
+ "",
261
+ ),
262
+ );
263
+ });
264
+ });
@@ -0,0 +1,161 @@
1
+ import { IRNode, LanguagePlugin } from "./types.js";
2
+ import {
3
+ RefTypeName,
4
+ RefTypes,
5
+ TypescriptBaseConverter,
6
+ } from "./TypescriptBaseConverter.js";
7
+ import { JSONSchema7Definition } from "json-schema";
8
+ import { JSONSchemaConverter } from "./JSONSchemaConverter.js";
9
+ import { PathUtils } from "./utils/path-utils.js";
10
+ import { toPascalCase } from "./utils/to-pascal-case.js";
11
+
12
+ /**
13
+ * A TypeScript language converter plugin.
14
+ */
15
+ export class TypescriptConverter
16
+ extends TypescriptBaseConverter
17
+ implements LanguagePlugin
18
+ {
19
+ public readonly language = "typescript";
20
+
21
+ private ir: JSONSchemaConverter;
22
+ private refTypes: RefTypes = [];
23
+
24
+ readonly code: string;
25
+
26
+ constructor(
27
+ schema: JSONSchema7Definition,
28
+ opts?: {
29
+ /**
30
+ * If true, referenced types will not be created for objects and instead
31
+ * the object type will be inlined.
32
+ */
33
+ inlineTypes?: boolean;
34
+ },
35
+ ) {
36
+ super();
37
+
38
+ this.ir = new JSONSchemaConverter(schema);
39
+
40
+ const code = this.generateType(this.ir.irNode, {
41
+ getReferencedType: opts?.inlineTypes
42
+ ? () => undefined
43
+ : this.getReferencedType.bind(this),
44
+ });
45
+
46
+ const rootName = this.ir.irNode.name ? this.ir.irNode.name : "Root";
47
+
48
+ this.code = `${this.refTypes
49
+ .map(([sig, name, { code }]) => `export type ${name} = ${code}`)
50
+ .join("\n")}
51
+
52
+ ${code}`;
53
+ }
54
+
55
+ private getUniqueRefTypeName(
56
+ signature: string,
57
+ nodePath: string,
58
+ ): RefTypeName {
59
+ const path = PathUtils.parsePath(nodePath);
60
+
61
+ // if the path ends in "0" it's an array item
62
+ const isArrayItem = nodePath.split(".").slice(-1)[0] === "0";
63
+
64
+ const postFixes = [
65
+ "Type",
66
+ "Element",
67
+ "Schema",
68
+ "Object",
69
+ "Shape",
70
+ "1",
71
+ "2",
72
+ "3",
73
+ "4",
74
+ "5",
75
+ "6",
76
+ ];
77
+
78
+ if (isArrayItem) {
79
+ postFixes.unshift("Item");
80
+ } else {
81
+ postFixes.unshift("");
82
+ }
83
+
84
+ let pathsSegmentsToInclude = 1;
85
+ let name = "";
86
+ let postFixIndexToTry = 0;
87
+
88
+ while (!name) {
89
+ const proposedName =
90
+ path.slice(-pathsSegmentsToInclude).map(toPascalCase).join("") +
91
+ postFixes[postFixIndexToTry];
92
+
93
+ const foundSignatureMatch = this.refTypes.find(([sig, name]) => {
94
+ return sig === signature && name === proposedName;
95
+ });
96
+
97
+ if (foundSignatureMatch) {
98
+ return foundSignatureMatch[1];
99
+ }
100
+
101
+ const nameAlreadyUsed = this.refTypes.find(([_, name]) => {
102
+ return name === proposedName;
103
+ });
104
+
105
+ if (nameAlreadyUsed) {
106
+ pathsSegmentsToInclude++;
107
+
108
+ if (pathsSegmentsToInclude >= path.length) {
109
+ // we're out of unique paths, increment a postfix and start the loop again
110
+ postFixIndexToTry++;
111
+ pathsSegmentsToInclude = 1;
112
+
113
+ // absolute fallback that should never/very-rarely happen
114
+ if (postFixIndexToTry === postFixes.length) {
115
+ name = proposedName + Math.floor(Math.random() * 1000);
116
+ }
117
+ }
118
+ } else {
119
+ name = proposedName;
120
+ }
121
+ }
122
+
123
+ return name;
124
+ }
125
+
126
+ protected getReferencedType(ir: IRNode): string | undefined {
127
+ const signature = ir.signature;
128
+
129
+ if (!signature) {
130
+ return undefined;
131
+ }
132
+
133
+ const name = this.getUniqueRefTypeName(signature, ir.path);
134
+
135
+ // account for recursion, the ref type could have already been created
136
+ if (
137
+ this.refTypes.find(([sig, _name]) => sig === signature && _name === name)
138
+ ) {
139
+ return name;
140
+ }
141
+
142
+ // push the sig/name pair to the refTypes array for recursion to avoid duplicates
143
+ this.refTypes.push([
144
+ signature,
145
+ name,
146
+ {
147
+ code: "",
148
+ },
149
+ ]);
150
+
151
+ const code = this.generateObjectType(ir, {
152
+ getReferencedType: this.getReferencedType.bind(this),
153
+ });
154
+
155
+ this.refTypes.find(
156
+ ([sig, _name]) => sig === signature && _name === name,
157
+ )![2].code = code;
158
+
159
+ return name;
160
+ }
161
+ }
@@ -0,0 +1,160 @@
1
+ import { IRNode, SignatureOccurrenceValue } from "./types.js";
2
+ import { JSONSchema7Definition } from "json-schema";
3
+ import { JSONSchemaConverter } from "./JSONSchemaConverter.js";
4
+ import {RefTypeName, RefTypes, TypescriptBaseConverter} from "./TypescriptBaseConverter.js";
5
+ import { PathUtils } from "./utils/path-utils.js";
6
+ import { toPascalCase } from "./utils/to-pascal-case.js";
7
+
8
+ /**
9
+ * A TypeScript language converter plugin that implements namespace scoping.
10
+ *
11
+ * Referenced/Shared typings are moved to the top level of the namespace scope
12
+ * that is created for each IR node.
13
+ */
14
+ export class TypescriptProcedureConverter extends TypescriptBaseConverter {
15
+ private ir: { args: JSONSchemaConverter; data: JSONSchemaConverter };
16
+ private refTypes: RefTypes = [];
17
+
18
+ readonly code: string;
19
+
20
+ constructor(
21
+ scopeName: string,
22
+ rpcJsonSchemas: {
23
+ args: JSONSchema7Definition;
24
+ data: JSONSchema7Definition;
25
+ },
26
+ ) {
27
+ super();
28
+
29
+ this.ir = {
30
+ args: new JSONSchemaConverter(rpcJsonSchemas.args),
31
+ data: new JSONSchemaConverter(rpcJsonSchemas.data),
32
+ };
33
+
34
+ const argsCode = this.generateObjectType(this.ir.args.irNode, {
35
+ getReferencedType: this.getReferencedType.bind(this),
36
+ });
37
+ const dataCode = this.generateObjectType(this.ir.data.irNode, {
38
+ getReferencedType: this.getReferencedType.bind(this),
39
+ });
40
+
41
+ this.code = `export namespace ${scopeName} {
42
+ export interface Args ${argsCode}
43
+
44
+ export interface Data ${dataCode}
45
+
46
+ ${this.refTypes
47
+ .map(([sig, name, { code }]) => `export type ${name} = ${code}`)
48
+ .join("\n")}
49
+ }`;
50
+ }
51
+
52
+ private getUniqueRefTypeName(
53
+ signature: string,
54
+ nodePath: string,
55
+ ): RefTypeName {
56
+ const path = PathUtils.parsePath(nodePath);
57
+
58
+ // if the path ends in "0" it's an array item
59
+ const isArrayItem = nodePath.split(".").slice(-1)[0] === "0";
60
+
61
+ const postFixes = [
62
+ "Type",
63
+ "Element",
64
+ "Schema",
65
+ "Object",
66
+ "Shape",
67
+ "1",
68
+ "2",
69
+ "3",
70
+ "4",
71
+ "5",
72
+ "6",
73
+ ];
74
+
75
+ if (isArrayItem) {
76
+ postFixes.unshift("Item");
77
+ // remove pluralization from the last segment
78
+ path[path.length - 1] = path[path.length - 1].replace(/s$/, "");
79
+ } else {
80
+ postFixes.unshift("");
81
+ }
82
+
83
+ let pathsSegmentsToInclude = 1;
84
+ let name = "";
85
+ let postFixIndexToTry = 0;
86
+
87
+ while (!name) {
88
+ const proposedName =
89
+ path.slice(-pathsSegmentsToInclude).map(toPascalCase).join("") +
90
+ postFixes[postFixIndexToTry];
91
+
92
+ const foundSignatureMatch = this.refTypes.find(([sig, name]) => {
93
+ return sig === signature && name === proposedName;
94
+ });
95
+
96
+ if (foundSignatureMatch) {
97
+ return foundSignatureMatch[1];
98
+ }
99
+
100
+ const nameAlreadyUsed = this.refTypes.find(([_, name]) => {
101
+ return name === proposedName;
102
+ });
103
+
104
+ if (nameAlreadyUsed) {
105
+ pathsSegmentsToInclude++;
106
+
107
+ if (pathsSegmentsToInclude >= path.length) {
108
+ // we're out of unique paths, increment a postfix and start the loop again
109
+ postFixIndexToTry++;
110
+ pathsSegmentsToInclude = 1;
111
+
112
+ // absolute fallback that should never/very-rarely happen
113
+ if (postFixIndexToTry === postFixes.length) {
114
+ name = proposedName + Math.floor(Math.random() * 1000);
115
+ }
116
+ }
117
+ } else {
118
+ name = proposedName;
119
+ }
120
+ }
121
+
122
+ return name;
123
+ }
124
+
125
+ protected getReferencedType(ir: IRNode): string | undefined {
126
+ const signature = ir.signature;
127
+
128
+ if (!signature) {
129
+ return undefined;
130
+ }
131
+
132
+ const name = this.getUniqueRefTypeName(signature, ir.path);
133
+
134
+ // account for recursion, the ref type could have already been created
135
+ if (
136
+ this.refTypes.find(([sig, _name]) => sig === signature && _name === name)
137
+ ) {
138
+ return name;
139
+ }
140
+
141
+ // push the sig/name pair to the refTypes array for recursion to avoid duplicates
142
+ this.refTypes.push([
143
+ signature,
144
+ name,
145
+ {
146
+ code: "",
147
+ },
148
+ ]);
149
+
150
+ const code = this.generateObjectType(ir, {
151
+ getReferencedType: this.getReferencedType.bind(this),
152
+ });
153
+
154
+ this.refTypes.find(
155
+ ([sig, _name]) => sig === signature && _name === name,
156
+ )![2].code = code;
157
+
158
+ return name;
159
+ }
160
+ }