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,459 @@
1
+ import { JSONSchema7, JSONSchema7Definition } from "json-schema";
2
+ import { ConverterOptions, IRNode, SignatureOccurrences } from "./types.js";
3
+
4
+ /**
5
+ * JSONSchemaConverter converts a JSON Schema (Draft‑07) into an
6
+ * intermediate representation (IR) that can later be transformed
7
+ * into target language code via a language plugin.
8
+ *
9
+ * This implementation now supports internal definitions via both
10
+ * "$defs" and "definitions", and resolves local "$ref" pointers.
11
+ */
12
+ export class JSONSchemaConverter {
13
+ private ir: IRNode;
14
+ // The root schema is stored to support reference resolution.
15
+ private rootSchema: JSONSchema7Definition | null = null;
16
+ // Keep track of the occurrences of each schema signature and the schema/property names
17
+ // used to reference the signature.
18
+ private signatureOccurrences: SignatureOccurrences = new Map();
19
+
20
+ get irNode(): IRNode {
21
+ return this.ir;
22
+ }
23
+
24
+ get signatureOccurrencesMap(): SignatureOccurrences {
25
+ return this.signatureOccurrences;
26
+ }
27
+
28
+ constructor(
29
+ schema: JSONSchema7Definition,
30
+ private opts?: ConverterOptions,
31
+ ) {
32
+ if (typeof schema === "object") {
33
+ schema.title = schema.title || "Root";
34
+ }
35
+
36
+ // Optionally validate the schema.
37
+ if (this.opts?.validateSchema) {
38
+ this.validateSchema(schema);
39
+ }
40
+
41
+ // Resolve references (for now, this is a placeholder).
42
+ const resolvedSchema = this.resolveReferences(schema);
43
+
44
+ // Store the root schema (if it is an object) to support local $ref resolution.
45
+ if (typeof resolvedSchema === "object") {
46
+ this.rootSchema = resolvedSchema;
47
+ }
48
+
49
+ // Convert the resolved schema to our intermediate representation.
50
+ let ir = this.convertToIR(resolvedSchema);
51
+
52
+ // add calculated signature occurrences by traversing the IR for all object types
53
+
54
+ // Apply any custom IR transformation.
55
+ if (this.opts?.transform) {
56
+ ir = this.opts.transform(ir);
57
+ }
58
+
59
+ this.ir = ir;
60
+ }
61
+
62
+ private calcObjSignatureOccurrences(
63
+ schema: JSONSchema7Definition & {
64
+ type: "object";
65
+ properties: Record<string, JSONSchema7Definition>;
66
+ },
67
+ node: IRNode & { name: string },
68
+ ): string {
69
+ // If no path then it's the root object
70
+ if (!node.path) {
71
+ return "";
72
+ }
73
+
74
+ const signature = this.getSchemaSignature(schema);
75
+
76
+ if (Object.keys(node.properties ?? {}).length === 0) {
77
+ // empty object - can be extended with options ie: emptyObjectAsUnknown
78
+ return signature;
79
+ }
80
+
81
+ if (this.signatureOccurrences.has(signature)) {
82
+ const occurrences = this.signatureOccurrences.get(signature)!;
83
+ occurrences.total += 1;
84
+
85
+ const foundPath = occurrences.occurrences.find(
86
+ (x) => x.nodePath === node.path,
87
+ );
88
+ if (foundPath) {
89
+ foundPath.count += 1;
90
+ } else {
91
+ occurrences.occurrences.push({ node, nodePath: node.path, count: 1 });
92
+ }
93
+
94
+ this.signatureOccurrences.set(signature, occurrences);
95
+ } else {
96
+ this.signatureOccurrences.set(signature, {
97
+ total: 1,
98
+ occurrences: [{ node, nodePath: node.path, count: 1 }],
99
+ signature,
100
+ });
101
+ }
102
+
103
+ return signature;
104
+ }
105
+
106
+ /**
107
+ * Recursively converts a JSON Schema definition into an IRNode.
108
+ *
109
+ * This method now handles $ref resolution (using the root schema),
110
+ * combinators (oneOf, anyOf, allOf), as well as enums, const, objects,
111
+ * arrays, and primitives.
112
+ */
113
+ private convertSchema(
114
+ schema: JSONSchema7Definition,
115
+ ctx: {
116
+ path: string;
117
+ },
118
+ ): IRNode {
119
+ if (!schema) {
120
+ // Return an unknown
121
+ return {
122
+ type: "null",
123
+ path: ctx.path,
124
+ };
125
+ }
126
+ // Handle boolean schemas.
127
+ if (typeof schema === "boolean") {
128
+ if (schema === true) {
129
+ // A schema of 'true' accepts any value.
130
+ return {
131
+ type: "object",
132
+ properties: {},
133
+ path: ctx.path,
134
+ };
135
+ } else {
136
+ throw new Error(
137
+ "Encountered JSON Schema 'false', which is not supported.",
138
+ );
139
+ }
140
+ }
141
+
142
+ // If the schema contains a $ref, resolve it.
143
+ if (schema.$ref) {
144
+ const resolved = this.resolveRef(schema);
145
+ return this.convertSchema(resolved, ctx);
146
+ }
147
+
148
+ // Handle combinators.
149
+ if (schema.oneOf) {
150
+ const options = schema.oneOf.map((subSchema) =>
151
+ this.convertSchema(subSchema, ctx),
152
+ );
153
+ return {
154
+ type: "union",
155
+ options,
156
+ constraints: { combinator: "oneOf" },
157
+ path: ctx.path,
158
+ };
159
+ }
160
+ if (schema.anyOf) {
161
+ const options = schema.anyOf.map((subSchema) =>
162
+ this.convertSchema(subSchema, ctx),
163
+ );
164
+ return {
165
+ type: "union",
166
+ options,
167
+ constraints: { combinator: "anyOf" },
168
+ path: ctx.path,
169
+ };
170
+ }
171
+ if (schema.allOf) {
172
+ const options = schema.allOf.map((subSchema) =>
173
+ this.convertSchema(subSchema, ctx),
174
+ );
175
+ return {
176
+ type: "intersection",
177
+ options,
178
+ path: ctx.path,
179
+ };
180
+ }
181
+
182
+ // Handle "enum".
183
+ if (schema.enum) {
184
+ return {
185
+ type: "enum",
186
+ values: schema.enum,
187
+ path: ctx.path,
188
+ };
189
+ }
190
+
191
+ // Handle "const".
192
+ if (schema.const !== undefined) {
193
+ return {
194
+ type: "literal",
195
+ constraints: { value: schema.const },
196
+ path: ctx.path,
197
+ };
198
+ }
199
+
200
+ // Process based on the "type" keyword.
201
+ if (schema.type) {
202
+ // If multiple types are provided, treat as a union.
203
+ if (Array.isArray(schema.type)) {
204
+ const options = schema.type.map((t) =>
205
+ this.convertSchema({ ...schema, type: t }, ctx),
206
+ );
207
+ return {
208
+ type: "union",
209
+ options,
210
+ path: ctx.path,
211
+ };
212
+ } else {
213
+ switch (schema.type) {
214
+ case "object": {
215
+ const name = this.getTypeName(schema, ctx);
216
+
217
+ const node: IRNode & {
218
+ name: string;
219
+ properties: {};
220
+ type: "object";
221
+ } = {
222
+ type: "object",
223
+ properties: {},
224
+ path: ctx.path,
225
+ name,
226
+ };
227
+
228
+ if (schema.properties) {
229
+ for (const key in schema.properties) {
230
+ const propSchema = schema.properties[key];
231
+
232
+ let child = this.convertSchema(propSchema, {
233
+ path: ctx.path ? `${ctx.path}.${key}` : key,
234
+ });
235
+
236
+ // Mark the property as required if listed in the "required" array.
237
+ child.required = schema.required
238
+ ? schema.required.includes(key)
239
+ : false;
240
+ node.properties![key] = child;
241
+ }
242
+ }
243
+
244
+ node.signature = this.calcObjSignatureOccurrences(
245
+ schema as JSONSchema7 & {
246
+ type: "object";
247
+ properties: Record<string, JSONSchema7Definition>;
248
+ },
249
+ node,
250
+ );
251
+
252
+ return node;
253
+ }
254
+ case "array": {
255
+ const name = this.getTypeName(schema, ctx);
256
+
257
+ const node: IRNode & { name: string } = {
258
+ type: "array",
259
+ path: ctx.path,
260
+ name,
261
+ };
262
+
263
+ if (schema.items) {
264
+ if (Array.isArray(schema.items)) {
265
+ // For tuple validation, treat as a union of the item types.
266
+ const options = schema.items.map((item, i) =>
267
+ this.convertSchema(item, ctx),
268
+ );
269
+ node.items = { type: "union", options, path: ctx.path };
270
+ } else {
271
+ node.items = this.convertSchema(schema.items, {
272
+ path: ctx.path ? `${ctx.path}.0` : "0",
273
+ });
274
+ }
275
+ }
276
+ return node;
277
+ }
278
+ case "string":
279
+ case "number":
280
+ case "integer":
281
+ case "boolean":
282
+ case "null": {
283
+ const node: IRNode = { type: schema.type, path: ctx.path };
284
+ node.constraints = {};
285
+ if (schema.pattern) node.constraints.pattern = schema.pattern;
286
+ if (schema.minLength !== undefined)
287
+ node.constraints.minLength = schema.minLength;
288
+ if (schema.maxLength !== undefined)
289
+ node.constraints.maxLength = schema.maxLength;
290
+ if (schema.minimum !== undefined)
291
+ node.constraints.minimum = schema.minimum;
292
+ if (schema.maximum !== undefined)
293
+ node.constraints.maximum = schema.maximum;
294
+ return node;
295
+ }
296
+ default:
297
+ throw new Error(
298
+ `Unsupported schema type: ${schema.type} - ${JSON.stringify(schema)}`,
299
+ );
300
+ }
301
+ }
302
+ }
303
+
304
+ // Fallback: if no type is provided, assume an object.
305
+ return { type: "object", properties: {}, path: ctx.path };
306
+ }
307
+
308
+ /**
309
+ * Converts a JSON Schema definition to target code using the provided language plugin.
310
+ */
311
+ public convertToIRSchema(schema: JSONSchema7Definition): IRNode {
312
+ // reset the root schema
313
+ this.rootSchema = null;
314
+
315
+ // Optionally validate the schema.
316
+ if (this.opts?.validateSchema) {
317
+ this.validateSchema(schema);
318
+ }
319
+
320
+ // Resolve references (for now, this is a placeholder).
321
+ const resolvedSchema = this.resolveReferences(schema);
322
+
323
+ // Store the root schema (if it is an object) to support local $ref resolution.
324
+ if (typeof resolvedSchema === "object") {
325
+ this.rootSchema = resolvedSchema;
326
+ }
327
+
328
+ // Convert the resolved schema to our intermediate representation.
329
+ let ir = this.convertToIR(resolvedSchema);
330
+
331
+ // Apply any custom IR transformation.
332
+ if (this.opts?.transform) {
333
+ ir = this.opts.transform(ir);
334
+ }
335
+
336
+ return ir;
337
+ }
338
+
339
+ private validateSchema(schema: JSONSchema7Definition): void {
340
+ // Implement or integrate with a JSON Schema validator if desired.
341
+ // For now, we assume the schema is valid.
342
+ }
343
+
344
+ /**
345
+ * A placeholder for reference resolution. In a more advanced implementation,
346
+ * this method might inline external references or perform more complex processing.
347
+ * ie: fetching a remote/url $def
348
+ */
349
+ private resolveReferences(
350
+ schema: JSONSchema7Definition,
351
+ ): JSONSchema7Definition {
352
+ // For this implementation, simply return the schema unchanged.
353
+ return schema;
354
+ }
355
+
356
+ private convertToIR(schema: JSONSchema7Definition): IRNode {
357
+ return this.convertSchema(schema, { path: "" });
358
+ }
359
+
360
+ private getNameFromPath(path: string): string | undefined {
361
+ // ignore numbers
362
+ const split = path.split(".").filter((x) => !x.match(/^\d+$/));
363
+
364
+ return split[split.length - 1] || undefined;
365
+ }
366
+
367
+ private getNextObjectSequence(): number {
368
+ return this.signatureOccurrences.size;
369
+ }
370
+
371
+ private getParentNameFromPath(path: string): string | undefined {
372
+ const split = path.split(".").filter((x) => !x.match(/^\d+$/));
373
+ // ignore numbers
374
+ return split[split.length - 2] || undefined;
375
+ }
376
+
377
+ /**
378
+ * Generates a unique signature for a schema.
379
+ * Uses a stable JSON.stringify that sorts keys to ensure that
380
+ * semantically equivalent schemas produce the same string.
381
+ */
382
+ private getSchemaSignature(schema: JSONSchema7Definition): string {
383
+ function sortKeys(obj: any): any {
384
+ if (typeof obj !== "object" || obj === null) return obj;
385
+ if (Array.isArray(obj)) return obj.map(sortKeys);
386
+ const sorted: any = {};
387
+ Object.keys(obj)
388
+ .sort()
389
+ .forEach((key) => {
390
+ sorted[key] = sortKeys(obj[key]);
391
+ });
392
+ return sorted;
393
+ }
394
+ return this.simpleHash(JSON.stringify(sortKeys(schema))).toString();
395
+ }
396
+
397
+ private getTypeName(schema: JSONSchema7, ctx: { path: string }): string {
398
+ return (
399
+ schema.title ||
400
+ this.getNameFromPath(ctx.path) ||
401
+ `Object${this.getNextObjectSequence()}`
402
+ );
403
+ }
404
+
405
+ /**
406
+ * Resolves a local $ref using the stored root schema.
407
+ *
408
+ * This method expects local references (starting with "#/") and uses a simple
409
+ * JSON Pointer resolution algorithm.
410
+ */
411
+ private resolveRef(schema: JSONSchema7): JSONSchema7Definition {
412
+ if (!schema.$ref) return schema as unknown as JSONSchema7Definition;
413
+ if (!this.rootSchema || typeof this.rootSchema !== "object") {
414
+ throw new Error("Root schema not available for reference resolution.");
415
+ }
416
+ const ref = schema.$ref;
417
+ if (!ref.startsWith("#/")) {
418
+ throw new Error(
419
+ `Only local references are supported. Encountered: ${ref}`,
420
+ );
421
+ }
422
+ return this.resolvePointer(ref, this.rootSchema);
423
+ }
424
+
425
+ /**
426
+ * Resolves a JSON Pointer (RFC 6901) within the given document.
427
+ *
428
+ * @param pointer A JSON Pointer string (e.g., "#/definitions/Foo" or "#/$defs/Bar")
429
+ * @param document The root document object.
430
+ */
431
+ private resolvePointer(pointer: string, document: any): any {
432
+ // Remove the leading '#' character.
433
+ if (pointer[0] === "#") {
434
+ pointer = pointer.substring(1);
435
+ }
436
+ if (!pointer) return document;
437
+ // Split the pointer by "/" and filter out empty parts.
438
+ const parts = pointer.split("/").filter((part) => part);
439
+ let current = document;
440
+ for (const part of parts) {
441
+ // Unescape any "~1" to "/" and "~0" to "~".
442
+ const unescaped = part.replace(/~1/g, "/").replace(/~0/g, "~");
443
+ current = current[unescaped];
444
+ if (current === undefined) {
445
+ throw new Error(`Reference "${pointer}" not found in document.`);
446
+ }
447
+ }
448
+ return current;
449
+ }
450
+
451
+ private simpleHash(str: string) {
452
+ let hash = 0;
453
+ for (let i = 0; i < str.length; i++) {
454
+ hash = (hash << 5) - hash + str.charCodeAt(i);
455
+ hash |= 0; // Convert to 32bit integer
456
+ }
457
+ return hash;
458
+ }
459
+ }
@@ -0,0 +1,161 @@
1
+ import { IRNode, SignatureOccurrenceValue } from "./types.js";
2
+ import { toPascalCase } from "./utils/to-pascal-case.js";
3
+
4
+ export type RefTypeName = string;
5
+
6
+ export type RefTypes = [
7
+ SignatureOccurrenceValue["signature"],
8
+ RefTypeName,
9
+ { code: string },
10
+ ][];
11
+
12
+ export class TypescriptBaseConverter {
13
+ /**
14
+ * Recursively generates a TypeScript type string for the given IR node.
15
+ *
16
+ * @param ir - The IR node to convert.
17
+ * @returns A string representing the TypeScript type.
18
+ */
19
+ protected generateType(
20
+ ir: IRNode,
21
+ utils: { getReferencedType(ir: IRNode): string | undefined },
22
+ ): string {
23
+ switch (ir.type) {
24
+ case "object":
25
+ const referencedType = utils.getReferencedType(ir);
26
+
27
+ if (referencedType) {
28
+ return referencedType;
29
+ }
30
+
31
+ return this.generateObjectType(ir, utils);
32
+ case "array":
33
+ if (ir.items) {
34
+ return `Array<${this.generateType(ir.items, utils)}>`;
35
+ }
36
+
37
+ return "any[]";
38
+ case "string":
39
+ return "string";
40
+ case "number":
41
+ case "integer":
42
+ return "number";
43
+ case "boolean":
44
+ return "boolean";
45
+ case "null":
46
+ return "null";
47
+ case "literal":
48
+ // For literal types, output a literal value (string, number, etc.)
49
+ return typeof ir.constraints?.value === "string"
50
+ ? JSON.stringify(ir.constraints.value)
51
+ : String(ir.constraints?.value);
52
+ case "enum":
53
+ // Create a union of literal types from the enum values.
54
+ if (ir.values && ir.values.length > 0) {
55
+ return ir.values.map((v) => JSON.stringify(v)).join(" | ");
56
+ }
57
+ return "never";
58
+ case "union":
59
+ if (ir.options && ir.options.length > 0) {
60
+ return ir.options
61
+ .map((opt) => this.generateType(opt, utils))
62
+ .join(" | ");
63
+ }
64
+ return "never";
65
+ case "intersection":
66
+ if (ir.options && ir.options.length > 0) {
67
+ return ir.options
68
+ .map((opt) => this.generateType(opt, utils))
69
+ .join(" & ");
70
+ }
71
+ return "never";
72
+ default:
73
+ return "any";
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Generates a TypeScript object type (as an inline type literal) from the given IR node.
79
+ *
80
+ * @param ir - The IR node of type "object".
81
+ * @returns A string representing the object type.
82
+ */
83
+ protected generateObjectType(
84
+ ir: IRNode,
85
+ utils: { getReferencedType(ir: IRNode): string | undefined },
86
+ ): string {
87
+ if (ir.properties) {
88
+ const props = Object.keys(ir.properties).map((key) => {
89
+ const prop = ir.properties![key];
90
+ // Append '?' if the property is not required.
91
+ const optional = prop.required ? "" : "?";
92
+ return `${this.isValidIdentifier(key) ? key : `"${key}"`}${optional}: ${this.generateType(prop, utils)};`;
93
+ });
94
+
95
+ if (!props.length) {
96
+ return "Record<string | number, unknown>";
97
+ }
98
+
99
+ return `{ ${props.join(" ")} }`;
100
+ }
101
+ return "Record<string | number, unknown>";
102
+ }
103
+
104
+ protected isValidIdentifier(name: string): boolean {
105
+ return /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(name);
106
+ }
107
+
108
+ /**
109
+ * Parses a list of dot-separated paths into an array of string arrays.
110
+ * Removes any "0" character segments.
111
+ *
112
+ * @param list
113
+ * @protected
114
+ */
115
+ protected parsePaths(list: string[]): string[][] {
116
+ return list.map((item) =>
117
+ item.split(".").filter((segment) => segment !== "0"),
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Returns the longest common prefix (sequence) among an array of string arrays.
123
+ * @param paths - An array of string arrays.
124
+ * @returns An array representing the common sequence.
125
+ *
126
+ * @example
127
+ * const arrays = [
128
+ * ["organizations", "migration", "step", "create_regions"],
129
+ * ["organizations", "migration", "step", "create_location_groups"],
130
+ * ["organizations", "migration", "step", "create_locations"],
131
+ * ["organizations", "migration", "step", "create_locations_bolos"],
132
+ * ];
133
+ *
134
+ * console.log(commonSequence(arrays)); // Output: ["organizations", "migration", "step"]
135
+ *
136
+ */
137
+ protected commonSequence(paths: string[][]): string[] {
138
+ if (paths.length === 0) return [];
139
+
140
+ const common: string[] = [];
141
+ for (let i = 0; i < paths[0].length; i++) {
142
+ const segment = paths[0][i];
143
+ if (paths.every((path) => path[i] === segment)) {
144
+ common.push(segment);
145
+ } else {
146
+ break;
147
+ }
148
+ }
149
+ return common;
150
+ }
151
+
152
+ protected makeTypeReferenceName(
153
+ signatureOccurrences: SignatureOccurrenceValue["occurrences"],
154
+ ): string {
155
+ const paths = this.parsePaths(
156
+ signatureOccurrences.map((occurrence) => occurrence.nodePath),
157
+ );
158
+ const common = this.commonSequence(paths);
159
+ return common.map((str) => toPascalCase(str)).join("");
160
+ }
161
+ }