@techspokes/typescript-wsdl-client 0.11.5 → 0.13.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.
- package/README.md +2 -1
- package/dist/app/generateApp.js +1 -1
- package/dist/client/generateClient.d.ts.map +1 -1
- package/dist/client/generateClient.js +9 -1
- package/dist/client/generateOperations.d.ts.map +1 -1
- package/dist/client/generateOperations.js +11 -1
- package/dist/client/generateTypes.d.ts.map +1 -1
- package/dist/client/generateTypes.js +49 -6
- package/dist/compiler/schemaCompiler.d.ts +8 -0
- package/dist/compiler/schemaCompiler.d.ts.map +1 -1
- package/dist/compiler/schemaCompiler.js +125 -12
- package/dist/gateway/generators.d.ts +2 -0
- package/dist/gateway/generators.d.ts.map +1 -1
- package/dist/gateway/generators.js +57 -23
- package/dist/gateway/helpers.d.ts +13 -0
- package/dist/gateway/helpers.d.ts.map +1 -1
- package/dist/gateway/helpers.js +58 -0
- package/dist/openapi/generatePaths.d.ts.map +1 -1
- package/dist/openapi/generatePaths.js +5 -1
- package/dist/openapi/generateSchemas.d.ts.map +1 -1
- package/dist/openapi/generateSchemas.js +29 -5
- package/dist/pipeline.d.ts +1 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +1 -0
- package/dist/test/generateTests.d.ts +1 -0
- package/dist/test/generateTests.d.ts.map +1 -1
- package/dist/test/generateTests.js +9 -4
- package/dist/test/generators.d.ts +12 -7
- package/dist/test/generators.d.ts.map +1 -1
- package/dist/test/generators.js +41 -37
- package/dist/test/mockData.d.ts +23 -10
- package/dist/test/mockData.d.ts.map +1 -1
- package/dist/test/mockData.js +44 -4
- package/dist/util/catalogMeta.d.ts +33 -0
- package/dist/util/catalogMeta.d.ts.map +1 -0
- package/dist/util/catalogMeta.js +95 -0
- package/docs/architecture.md +2 -0
- package/docs/concepts.md +2 -0
- package/docs/generated-code.md +31 -3
- package/docs/testing.md +10 -9
- package/package.json +7 -7
package/dist/test/generators.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* via computeRelativeImport() from src/util/imports.ts.
|
|
10
10
|
*/
|
|
11
11
|
import { computeRelativeImport } from "../util/imports.js";
|
|
12
|
-
import {
|
|
12
|
+
import { detectArrayWrappers, detectChildrenTypes } from "../util/catalogMeta.js";
|
|
13
13
|
/**
|
|
14
14
|
* Emits vitest.config.ts content.
|
|
15
15
|
*
|
|
@@ -17,12 +17,13 @@ import { generateAllOperationMocks } from "./mockData.js";
|
|
|
17
17
|
* config file location, not the working directory.
|
|
18
18
|
*/
|
|
19
19
|
export function emitVitestConfig() {
|
|
20
|
-
return `import {
|
|
20
|
+
return `import { defineProject } from "vitest/config";
|
|
21
|
+
import { dirname } from "node:path";
|
|
21
22
|
import { fileURLToPath } from "node:url";
|
|
22
23
|
|
|
23
24
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
25
|
|
|
25
|
-
export default {
|
|
26
|
+
export default defineProject({
|
|
26
27
|
test: {
|
|
27
28
|
root: __dirname,
|
|
28
29
|
include: [
|
|
@@ -31,7 +32,7 @@ export default {
|
|
|
31
32
|
],
|
|
32
33
|
testTimeout: 30000,
|
|
33
34
|
},
|
|
34
|
-
};
|
|
35
|
+
});
|
|
35
36
|
`;
|
|
36
37
|
}
|
|
37
38
|
/**
|
|
@@ -42,12 +43,11 @@ export default {
|
|
|
42
43
|
* @param importsMode - Import extension mode
|
|
43
44
|
* @param clientMeta - Client metadata
|
|
44
45
|
* @param operations - Resolved operation metadata
|
|
45
|
-
* @param
|
|
46
|
+
* @param mocks - Pre-computed mock data map
|
|
46
47
|
*/
|
|
47
|
-
export function emitMockClientHelper(testDir, clientDir, importsMode, clientMeta, operations,
|
|
48
|
+
export function emitMockClientHelper(testDir, clientDir, importsMode, clientMeta, operations, mocks) {
|
|
48
49
|
const helpersDir = `${testDir}/helpers`;
|
|
49
50
|
const operationsImport = computeRelativeImport(helpersDir, `${clientDir}/operations`, importsMode);
|
|
50
|
-
const mocks = generateAllOperationMocks(catalog);
|
|
51
51
|
// Sort operations for deterministic output
|
|
52
52
|
const sortedOps = [...operations].sort((a, b) => a.operationId.localeCompare(b.operationId));
|
|
53
53
|
const methodEntries = sortedOps.map((op) => {
|
|
@@ -141,12 +141,12 @@ export async function createTestApp(
|
|
|
141
141
|
* @param testDir - Absolute path to test output directory
|
|
142
142
|
* @param importsMode - Import extension mode
|
|
143
143
|
* @param operations - Resolved operation metadata
|
|
144
|
-
* @param
|
|
144
|
+
* @param mocks - Pre-computed mock data map
|
|
145
145
|
*/
|
|
146
|
-
|
|
146
|
+
// noinspection JSUnusedLocalSymbols
|
|
147
|
+
export function emitRoutesTest(testDir, importsMode, operations, mocks) {
|
|
147
148
|
const suffix = importsMode === "bare" ? "" : `.${importsMode}`;
|
|
148
149
|
const testAppImport = `../helpers/test-app${suffix}`;
|
|
149
|
-
const mocks = generateAllOperationMocks(catalog);
|
|
150
150
|
// Sort operations for deterministic output
|
|
151
151
|
const sortedOps = [...operations].sort((a, b) => a.operationId.localeCompare(b.operationId));
|
|
152
152
|
const testCases = sortedOps.map((op) => {
|
|
@@ -191,9 +191,10 @@ ${testCases}
|
|
|
191
191
|
* @param testDir - Absolute path to test output directory
|
|
192
192
|
* @param importsMode - Import extension mode
|
|
193
193
|
* @param operations - Resolved operation metadata (uses first operation for error tests)
|
|
194
|
-
* @param
|
|
194
|
+
* @param mocks - Pre-computed mock data map
|
|
195
195
|
*/
|
|
196
|
-
|
|
196
|
+
// noinspection JSUnusedLocalSymbols
|
|
197
|
+
export function emitErrorsTest(testDir, importsMode, operations, mocks) {
|
|
197
198
|
const suffix = importsMode === "bare" ? "" : `.${importsMode}`;
|
|
198
199
|
const testAppImport = `../helpers/test-app${suffix}`;
|
|
199
200
|
const mockClientImport = `../helpers/mock-client${suffix}`;
|
|
@@ -202,7 +203,6 @@ export function emitErrorsTest(testDir, importsMode, operations, catalog) {
|
|
|
202
203
|
const op = sortedOps[0];
|
|
203
204
|
if (!op)
|
|
204
205
|
return "// No operations found\n";
|
|
205
|
-
const mocks = generateAllOperationMocks(catalog);
|
|
206
206
|
const mockData = mocks.get(op.operationId);
|
|
207
207
|
const requestPayload = JSON.stringify(mockData?.request ?? {}, null, 4).replace(/\n/g, "\n ");
|
|
208
208
|
return `/**
|
|
@@ -325,7 +325,8 @@ describe("gateway routes — error handling", () => {
|
|
|
325
325
|
/**
|
|
326
326
|
* Emits gateway/envelope.test.ts with SUCCESS/ERROR structure assertions.
|
|
327
327
|
*/
|
|
328
|
-
|
|
328
|
+
// noinspection JSUnusedLocalSymbols
|
|
329
|
+
export function emitEnvelopeTest(testDir, importsMode, operations, mocks) {
|
|
329
330
|
// noinspection DuplicatedCode
|
|
330
331
|
const suffix = importsMode === "bare" ? "" : `.${importsMode}`;
|
|
331
332
|
const testAppImport = `../helpers/test-app${suffix}`;
|
|
@@ -334,7 +335,6 @@ export function emitEnvelopeTest(testDir, importsMode, operations, catalog) {
|
|
|
334
335
|
const op = sortedOps[0];
|
|
335
336
|
if (!op)
|
|
336
337
|
return "// No operations found\n";
|
|
337
|
-
const mocks = generateAllOperationMocks(catalog);
|
|
338
338
|
const mockData = mocks.get(op.operationId);
|
|
339
339
|
const requestPayload = JSON.stringify(mockData?.request ?? {}, null, 4).replace(/\n/g, "\n ");
|
|
340
340
|
return `/**
|
|
@@ -399,6 +399,7 @@ describe("gateway — envelope structure", () => {
|
|
|
399
399
|
/**
|
|
400
400
|
* Emits gateway/validation.test.ts with invalid payload tests per route.
|
|
401
401
|
*/
|
|
402
|
+
// noinspection JSUnusedLocalSymbols
|
|
402
403
|
export function emitValidationTest(testDir, importsMode, operations) {
|
|
403
404
|
const suffix = importsMode === "bare" ? "" : `.${importsMode}`;
|
|
404
405
|
const testAppImport = `../helpers/test-app${suffix}`;
|
|
@@ -572,10 +573,14 @@ describe("buildErrorEnvelope", () => {
|
|
|
572
573
|
* Returns null if no array wrappers exist in the catalog.
|
|
573
574
|
*/
|
|
574
575
|
export function emitUnwrapTest(testDir, gatewayDir, importsMode, catalog) {
|
|
575
|
-
// Detect array wrappers
|
|
576
|
-
const
|
|
576
|
+
// Detect array wrappers using shared utility with full type definitions
|
|
577
|
+
const types = catalog.types ?? [];
|
|
578
|
+
const wrappers = detectArrayWrappers(types);
|
|
577
579
|
if (Object.keys(wrappers).length === 0)
|
|
578
580
|
return null;
|
|
581
|
+
// Detect children-only types (in CHILDREN_TYPES but NOT in ARRAY_WRAPPERS)
|
|
582
|
+
const childTypeMap = catalog.meta?.childType ?? {};
|
|
583
|
+
const childrenOnly = detectChildrenTypes(childTypeMap, wrappers);
|
|
579
584
|
const runtimeDir = `${testDir}/runtime`;
|
|
580
585
|
const runtimeImport = computeRelativeImport(runtimeDir, `${gatewayDir}/runtime`, importsMode);
|
|
581
586
|
const wrapperTests = Object.entries(wrappers).map(([wrapperType, innerKey]) => {
|
|
@@ -585,6 +590,24 @@ export function emitUnwrapTest(testDir, gatewayDir, importsMode, catalog) {
|
|
|
585
590
|
expect(result).toEqual([{ id: 1 }]);
|
|
586
591
|
});`;
|
|
587
592
|
}).join("\n\n");
|
|
593
|
+
// Generate tests for children-only types that have nested wrappers
|
|
594
|
+
const childrenTests = [];
|
|
595
|
+
for (const [typeName, children] of Object.entries(childrenOnly)) {
|
|
596
|
+
// Find children whose types are array wrappers (these get recursively unwrapped)
|
|
597
|
+
const wrappedChildren = Object.entries(children).filter(([, childType]) => childType in wrappers);
|
|
598
|
+
if (wrappedChildren.length === 0)
|
|
599
|
+
continue;
|
|
600
|
+
const [childProp, childType] = wrappedChildren[0];
|
|
601
|
+
const innerKey = wrappers[childType];
|
|
602
|
+
childrenTests.push(` it("recursively unwraps nested wrappers in ${typeName}", () => {
|
|
603
|
+
const input = { ${childProp}: { ${innerKey}: [{ id: 1 }] } };
|
|
604
|
+
const result = unwrapArrayWrappers(input, "${typeName}");
|
|
605
|
+
expect(result).toEqual({ ${childProp}: [{ id: 1 }] });
|
|
606
|
+
});`);
|
|
607
|
+
}
|
|
608
|
+
const childrenTestBlock = childrenTests.length > 0
|
|
609
|
+
? "\n" + childrenTests.join("\n\n") + "\n"
|
|
610
|
+
: "";
|
|
588
611
|
return `/**
|
|
589
612
|
* unwrapArrayWrappers() Unit Tests
|
|
590
613
|
*
|
|
@@ -597,7 +620,7 @@ import { unwrapArrayWrappers } from "${runtimeImport}";
|
|
|
597
620
|
|
|
598
621
|
describe("unwrapArrayWrappers", () => {
|
|
599
622
|
${wrapperTests}
|
|
600
|
-
|
|
623
|
+
${childrenTestBlock}
|
|
601
624
|
it("returns empty array when inner key is missing", () => {
|
|
602
625
|
const result = unwrapArrayWrappers({}, "${Object.keys(wrappers)[0]}");
|
|
603
626
|
expect(result).toEqual([]);
|
|
@@ -611,22 +634,3 @@ ${wrapperTests}
|
|
|
611
634
|
});
|
|
612
635
|
`;
|
|
613
636
|
}
|
|
614
|
-
/**
|
|
615
|
-
* Detects ArrayOf* wrapper types from catalog (mirrors the logic in generators.ts).
|
|
616
|
-
*/
|
|
617
|
-
function detectArrayWrappersFromCatalog(catalog) {
|
|
618
|
-
const wrappers = {};
|
|
619
|
-
const childTypes = catalog.meta?.childType ?? {};
|
|
620
|
-
const propMeta = catalog.meta?.propMeta ?? {};
|
|
621
|
-
for (const [typeName, children] of Object.entries(childTypes)) {
|
|
622
|
-
const childEntries = Object.entries(children);
|
|
623
|
-
if (childEntries.length !== 1)
|
|
624
|
-
continue;
|
|
625
|
-
const [propName] = childEntries[0];
|
|
626
|
-
const meta = propMeta[typeName]?.[propName];
|
|
627
|
-
if (meta?.max === "unbounded" || (typeof meta?.max === "number" && meta.max > 1)) {
|
|
628
|
-
wrappers[typeName] = propName;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
return wrappers;
|
|
632
|
-
}
|
package/dist/test/mockData.d.ts
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mock Data Generator
|
|
3
|
-
*
|
|
4
|
-
* Generates realistic mock data trees from compiled catalog type metadata.
|
|
5
|
-
* Used by the test generator to create full default responses per operation
|
|
6
|
-
* so that generated tests pass out of the box.
|
|
7
|
-
*
|
|
8
|
-
* Pure logic — no I/O or side effects.
|
|
9
|
-
*/
|
|
10
1
|
/**
|
|
11
2
|
* Options for mock data generation
|
|
12
3
|
*/
|
|
@@ -18,6 +9,7 @@ export interface MockDataOptions {
|
|
|
18
9
|
*/
|
|
19
10
|
export interface CatalogForMocks {
|
|
20
11
|
meta?: {
|
|
12
|
+
attrType?: Record<string, Record<string, string>>;
|
|
21
13
|
childType?: Record<string, Record<string, string>>;
|
|
22
14
|
propMeta?: Record<string, Record<string, {
|
|
23
15
|
declaredType?: string;
|
|
@@ -31,6 +23,16 @@ export interface CatalogForMocks {
|
|
|
31
23
|
inputTypeName?: string;
|
|
32
24
|
outputTypeName?: string;
|
|
33
25
|
}>;
|
|
26
|
+
types?: Array<{
|
|
27
|
+
name: string;
|
|
28
|
+
attrs: Array<{
|
|
29
|
+
name: string;
|
|
30
|
+
}>;
|
|
31
|
+
elems: Array<{
|
|
32
|
+
name: string;
|
|
33
|
+
max: number | "unbounded";
|
|
34
|
+
}>;
|
|
35
|
+
}>;
|
|
34
36
|
}
|
|
35
37
|
/**
|
|
36
38
|
* Generates a mock primitive value based on the TypeScript type and property name.
|
|
@@ -52,16 +54,27 @@ export declare function generateMockPrimitive(tsType: string, propName: string):
|
|
|
52
54
|
* @returns Mock data object matching the type structure
|
|
53
55
|
*/
|
|
54
56
|
export declare function generateMockData(typeName: string, catalog: CatalogForMocks, opts?: MockDataOptions, visited?: Set<string>, depth?: number): Record<string, unknown>;
|
|
57
|
+
/**
|
|
58
|
+
* Options for bulk mock generation
|
|
59
|
+
*/
|
|
60
|
+
export interface GenerateAllMocksOptions {
|
|
61
|
+
flattenArrayWrappers?: boolean;
|
|
62
|
+
}
|
|
55
63
|
/**
|
|
56
64
|
* Generates mock request and response data for all operations in the catalog.
|
|
57
65
|
*
|
|
58
66
|
* Response data uses the pre-unwrap shape (e.g. { WeatherDescription: [{...}] }
|
|
59
67
|
* not [{...}]) since the generated route handler calls unwrapArrayWrappers() at runtime.
|
|
60
68
|
*
|
|
69
|
+
* When flattenArrayWrappers is enabled, request payloads are post-processed to
|
|
70
|
+
* flatten ArrayOf* wrapper objects into plain arrays, matching the OpenAPI schema
|
|
71
|
+
* shape that AJV validates against.
|
|
72
|
+
*
|
|
61
73
|
* @param catalog - The compiled catalog with operations and type metadata
|
|
74
|
+
* @param opts - Optional generation options
|
|
62
75
|
* @returns Map from operation name to { request, response } mock data
|
|
63
76
|
*/
|
|
64
|
-
export declare function generateAllOperationMocks(catalog: CatalogForMocks): Map<string, {
|
|
77
|
+
export declare function generateAllOperationMocks(catalog: CatalogForMocks, opts?: GenerateAllMocksOptions): Map<string, {
|
|
65
78
|
request: Record<string, unknown>;
|
|
66
79
|
response: Record<string, unknown>;
|
|
67
80
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mockData.d.ts","sourceRoot":"","sources":["../../src/test/mockData.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mockData.d.ts","sourceRoot":"","sources":["../../src/test/mockData.ts"],"names":[],"mappings":"AAWA;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE;QACL,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;QAClD,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;QACnD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;YACvC,YAAY,CAAC,EAAE,MAAM,CAAC;YACtB,GAAG,CAAC,EAAE,MAAM,CAAC;YACb,GAAG,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;YAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;SACpB,CAAC,CAAC,CAAC;KACL,CAAC;IACF,UAAU,CAAC,EAAE,KAAK,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC,CAAC;IACH,KAAK,CAAC,EAAE,KAAK,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QAC/B,KAAK,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAAA;SAAE,CAAC,CAAC;KAC3D,CAAC,CAAC;CACJ;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAwCjG;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,eAAe,EACxB,IAAI,CAAC,EAAE,eAAe,EACtB,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EACrB,KAAK,CAAC,EAAE,MAAM,GACb,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAoDzB;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,eAAe,EACxB,IAAI,CAAC,EAAE,uBAAuB,GAC7B,GAAG,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,CA+BtF"}
|
package/dist/test/mockData.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Pure logic — no I/O or side effects.
|
|
9
9
|
*/
|
|
10
|
+
import { detectArrayWrappers, flattenMockPayload } from "../util/catalogMeta.js";
|
|
10
11
|
/**
|
|
11
12
|
* Generates a mock primitive value based on the TypeScript type and property name.
|
|
12
13
|
* Uses contextual defaults based on common property names.
|
|
@@ -33,6 +34,13 @@ export function generateMockPrimitive(tsType, propName) {
|
|
|
33
34
|
return 1013;
|
|
34
35
|
return 0;
|
|
35
36
|
}
|
|
37
|
+
// String union / enum type — pick the first literal value
|
|
38
|
+
// e.g. '"Test" | "Production"' → "Test"
|
|
39
|
+
if (tsType.includes("|") && tsType.includes('"')) {
|
|
40
|
+
const match = tsType.match(/"([^"]+)"/);
|
|
41
|
+
if (match)
|
|
42
|
+
return match[1];
|
|
43
|
+
}
|
|
36
44
|
// string type — use contextual defaults
|
|
37
45
|
if (lower === "zip" || lower === "zipcode" || lower === "postalcode")
|
|
38
46
|
return "10001";
|
|
@@ -84,14 +92,15 @@ export function generateMockData(typeName, catalog, opts, visited, depth) {
|
|
|
84
92
|
return {};
|
|
85
93
|
}
|
|
86
94
|
const childTypes = catalog.meta?.childType?.[typeName];
|
|
87
|
-
|
|
95
|
+
const attrTypes = catalog.meta?.attrType?.[typeName];
|
|
96
|
+
if ((!childTypes || Object.keys(childTypes).length === 0) && !attrTypes) {
|
|
88
97
|
return {};
|
|
89
98
|
}
|
|
90
99
|
const propMeta = catalog.meta?.propMeta?.[typeName] ?? {};
|
|
91
100
|
const newVisited = new Set(currentVisited);
|
|
92
101
|
newVisited.add(typeName);
|
|
93
102
|
const result = {};
|
|
94
|
-
for (const [propName, propType] of Object.entries(childTypes)) {
|
|
103
|
+
for (const [propName, propType] of Object.entries(childTypes ?? {})) {
|
|
95
104
|
const meta = propMeta[propName];
|
|
96
105
|
const isArray = meta?.max === "unbounded" || (typeof meta?.max === "number" && meta.max > 1);
|
|
97
106
|
// Check if it's a primitive type
|
|
@@ -105,6 +114,21 @@ export function generateMockData(typeName, catalog, opts, visited, depth) {
|
|
|
105
114
|
result[propName] = isArray ? [childData] : childData;
|
|
106
115
|
}
|
|
107
116
|
}
|
|
117
|
+
// Include XML attributes (not in childType, stored separately in attrType)
|
|
118
|
+
if (attrTypes) {
|
|
119
|
+
for (const [attrName, attrTsType] of Object.entries(attrTypes)) {
|
|
120
|
+
if (!(attrName in result)) {
|
|
121
|
+
// Handle array-typed attributes (e.g. "string[]")
|
|
122
|
+
if (attrTsType.endsWith("[]")) {
|
|
123
|
+
const baseType = attrTsType.slice(0, -2);
|
|
124
|
+
result[attrName] = [generateMockPrimitive(baseType, attrName)];
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
result[attrName] = generateMockPrimitive(attrTsType, attrName);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
108
132
|
return result;
|
|
109
133
|
}
|
|
110
134
|
/**
|
|
@@ -113,17 +137,33 @@ export function generateMockData(typeName, catalog, opts, visited, depth) {
|
|
|
113
137
|
* Response data uses the pre-unwrap shape (e.g. { WeatherDescription: [{...}] }
|
|
114
138
|
* not [{...}]) since the generated route handler calls unwrapArrayWrappers() at runtime.
|
|
115
139
|
*
|
|
140
|
+
* When flattenArrayWrappers is enabled, request payloads are post-processed to
|
|
141
|
+
* flatten ArrayOf* wrapper objects into plain arrays, matching the OpenAPI schema
|
|
142
|
+
* shape that AJV validates against.
|
|
143
|
+
*
|
|
116
144
|
* @param catalog - The compiled catalog with operations and type metadata
|
|
145
|
+
* @param opts - Optional generation options
|
|
117
146
|
* @returns Map from operation name to { request, response } mock data
|
|
118
147
|
*/
|
|
119
|
-
export function generateAllOperationMocks(catalog) {
|
|
148
|
+
export function generateAllOperationMocks(catalog, opts) {
|
|
120
149
|
const result = new Map();
|
|
121
150
|
if (!catalog.operations)
|
|
122
151
|
return result;
|
|
152
|
+
// Detect array wrappers once for all operations
|
|
153
|
+
const arrayWrappers = opts?.flattenArrayWrappers !== false && catalog.types
|
|
154
|
+
? detectArrayWrappers(catalog.types)
|
|
155
|
+
: {};
|
|
156
|
+
const shouldFlatten = Object.keys(arrayWrappers).length > 0;
|
|
157
|
+
const childTypeMap = catalog.meta?.childType ?? {};
|
|
123
158
|
for (const op of catalog.operations) {
|
|
124
|
-
|
|
159
|
+
let request = op.inputTypeName
|
|
125
160
|
? generateMockData(op.inputTypeName, catalog)
|
|
126
161
|
: {};
|
|
162
|
+
// Flatten request payloads when array wrappers are active
|
|
163
|
+
if (shouldFlatten && op.inputTypeName) {
|
|
164
|
+
request = flattenMockPayload(request, op.inputTypeName, childTypeMap, arrayWrappers);
|
|
165
|
+
}
|
|
166
|
+
// Response stays SOAP-shaped (pre-unwrap) since runtime unwrapArrayWrappers() handles it
|
|
127
167
|
const response = op.outputTypeName
|
|
128
168
|
? generateMockData(op.outputTypeName, catalog)
|
|
129
169
|
: {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared catalog analysis utilities.
|
|
3
|
+
*
|
|
4
|
+
* Provides canonical array-wrapper and children-type detection used by both
|
|
5
|
+
* the gateway generator (runtime.ts emission) and the test generator.
|
|
6
|
+
*/
|
|
7
|
+
export interface CatalogTypeDef {
|
|
8
|
+
name: string;
|
|
9
|
+
attrs: Array<{
|
|
10
|
+
name: string;
|
|
11
|
+
}>;
|
|
12
|
+
elems: Array<{
|
|
13
|
+
name: string;
|
|
14
|
+
max: number | "unbounded";
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Detects ArrayOf* wrapper types: exactly 1 element with max unbounded, no attributes.
|
|
19
|
+
* Returns Record<wrapperTypeName, innerPropertyName>.
|
|
20
|
+
*/
|
|
21
|
+
export declare function detectArrayWrappers(types: CatalogTypeDef[]): Record<string, string>;
|
|
22
|
+
/**
|
|
23
|
+
* Builds CHILDREN_TYPES map: all childType entries that are NOT array wrappers.
|
|
24
|
+
* Used by the runtime unwrapArrayWrappers() for recursive child processing.
|
|
25
|
+
*/
|
|
26
|
+
export declare function detectChildrenTypes(childTypeMap: Record<string, Record<string, string>>, arrayWrappers: Record<string, string>): Record<string, Record<string, string>>;
|
|
27
|
+
/**
|
|
28
|
+
* Post-processes a mock request payload to flatten array-wrapper fields.
|
|
29
|
+
* Replaces { InnerProp: [...items] } with [...items] for wrapper types.
|
|
30
|
+
* Recursion follows the childType map.
|
|
31
|
+
*/
|
|
32
|
+
export declare function flattenMockPayload(data: Record<string, unknown>, typeName: string, childTypeMap: Record<string, Record<string, string>>, arrayWrappers: Record<string, string>): Record<string, unknown>;
|
|
33
|
+
//# sourceMappingURL=catalogMeta.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalogMeta.d.ts","sourceRoot":"","sources":["../../src/util/catalogMeta.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/B,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAAA;KAAE,CAAC,CAAC;CAC3D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUnF;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EACpD,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAOxC;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EACpD,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAoDzB"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared catalog analysis utilities.
|
|
3
|
+
*
|
|
4
|
+
* Provides canonical array-wrapper and children-type detection used by both
|
|
5
|
+
* the gateway generator (runtime.ts emission) and the test generator.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Detects ArrayOf* wrapper types: exactly 1 element with max unbounded, no attributes.
|
|
9
|
+
* Returns Record<wrapperTypeName, innerPropertyName>.
|
|
10
|
+
*/
|
|
11
|
+
export function detectArrayWrappers(types) {
|
|
12
|
+
const wrappers = {};
|
|
13
|
+
for (const t of types) {
|
|
14
|
+
if (t.attrs && t.attrs.length !== 0)
|
|
15
|
+
continue;
|
|
16
|
+
if (!t.elems || t.elems.length !== 1)
|
|
17
|
+
continue;
|
|
18
|
+
const e = t.elems[0];
|
|
19
|
+
if (e.max !== "unbounded" && !(typeof e.max === "number" && e.max > 1))
|
|
20
|
+
continue;
|
|
21
|
+
wrappers[t.name] = e.name;
|
|
22
|
+
}
|
|
23
|
+
return wrappers;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Builds CHILDREN_TYPES map: all childType entries that are NOT array wrappers.
|
|
27
|
+
* Used by the runtime unwrapArrayWrappers() for recursive child processing.
|
|
28
|
+
*/
|
|
29
|
+
export function detectChildrenTypes(childTypeMap, arrayWrappers) {
|
|
30
|
+
const result = {};
|
|
31
|
+
for (const [typeName, children] of Object.entries(childTypeMap)) {
|
|
32
|
+
if (typeName in arrayWrappers)
|
|
33
|
+
continue;
|
|
34
|
+
result[typeName] = children;
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Post-processes a mock request payload to flatten array-wrapper fields.
|
|
40
|
+
* Replaces { InnerProp: [...items] } with [...items] for wrapper types.
|
|
41
|
+
* Recursion follows the childType map.
|
|
42
|
+
*/
|
|
43
|
+
export function flattenMockPayload(data, typeName, childTypeMap, arrayWrappers) {
|
|
44
|
+
const children = childTypeMap[typeName];
|
|
45
|
+
if (!children)
|
|
46
|
+
return data;
|
|
47
|
+
const result = {};
|
|
48
|
+
for (const [key, value] of Object.entries(data)) {
|
|
49
|
+
const childTypeName = children[key];
|
|
50
|
+
if (childTypeName && childTypeName in arrayWrappers && value != null && typeof value === "object" && !Array.isArray(value)) {
|
|
51
|
+
// This field's type is an array wrapper — flatten it
|
|
52
|
+
const innerKey = arrayWrappers[childTypeName];
|
|
53
|
+
const innerValue = value[innerKey];
|
|
54
|
+
// Recurse into each item of the unwrapped array
|
|
55
|
+
const itemTypeName = childTypeMap[childTypeName]?.[innerKey];
|
|
56
|
+
if (Array.isArray(innerValue) && itemTypeName) {
|
|
57
|
+
result[key] = innerValue.map(item => item != null && typeof item === "object" && !Array.isArray(item)
|
|
58
|
+
? flattenMockPayload(item, itemTypeName, childTypeMap, arrayWrappers)
|
|
59
|
+
: item);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
result[key] = innerValue ?? [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (childTypeName && value != null && typeof value === "object" && !Array.isArray(value)) {
|
|
66
|
+
// Recurse into complex types
|
|
67
|
+
result[key] = flattenMockPayload(value, childTypeName, childTypeMap, arrayWrappers);
|
|
68
|
+
}
|
|
69
|
+
else if (childTypeName && Array.isArray(value)) {
|
|
70
|
+
// Array of complex types — flatten each item
|
|
71
|
+
result[key] = value.map(item => {
|
|
72
|
+
if (item != null && typeof item === "object" && !Array.isArray(item)) {
|
|
73
|
+
if (childTypeName in arrayWrappers) {
|
|
74
|
+
const innerKey = arrayWrappers[childTypeName];
|
|
75
|
+
const innerValue = item[innerKey];
|
|
76
|
+
// Recurse into each item of the unwrapped array
|
|
77
|
+
const itemTypeName = childTypeMap[childTypeName]?.[innerKey];
|
|
78
|
+
if (Array.isArray(innerValue) && itemTypeName) {
|
|
79
|
+
return innerValue.map(subItem => subItem != null && typeof subItem === "object" && !Array.isArray(subItem)
|
|
80
|
+
? flattenMockPayload(subItem, itemTypeName, childTypeMap, arrayWrappers)
|
|
81
|
+
: subItem);
|
|
82
|
+
}
|
|
83
|
+
return innerValue ?? [];
|
|
84
|
+
}
|
|
85
|
+
return flattenMockPayload(item, childTypeName, childTypeMap, arrayWrappers);
|
|
86
|
+
}
|
|
87
|
+
return item;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
result[key] = value;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
package/docs/architecture.md
CHANGED
|
@@ -83,6 +83,8 @@ CompiledCatalog {
|
|
|
83
83
|
}
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
`CompiledType`, type aliases, and operations may include optional `doc` fields populated from WSDL/XSD documentation nodes. Client emitters consume these values to generate comments in `types.ts`, `operations.ts`, and `client.ts`. OpenAPI emitters consume the same fields for schema, property, and operation `description` values.
|
|
87
|
+
|
|
86
88
|
Data flow through the pipeline:
|
|
87
89
|
|
|
88
90
|
1. schemaCompiler produces CompiledCatalog from WSDL XML
|
package/docs/concepts.md
CHANGED
|
@@ -67,6 +67,8 @@ cacheable, and reused across client, OpenAPI, and gateway generation.
|
|
|
67
67
|
Inspect types, operations, and metadata as plain JSON. The catalog is
|
|
68
68
|
automatically placed alongside generated output.
|
|
69
69
|
|
|
70
|
+
The catalog stores optional human-readable `doc` fields extracted from WSDL/XSD documentation nodes. These fields are additive metadata used by TypeScript and OpenAPI emitters and do not change runtime behavior.
|
|
71
|
+
|
|
70
72
|
### Catalog Locations by Command
|
|
71
73
|
|
|
72
74
|
| Command | Location |
|
package/docs/generated-code.md
CHANGED
|
@@ -73,6 +73,34 @@ result.GetCityWeatherByZIPResult.Temperature;
|
|
|
73
73
|
|
|
74
74
|
Autocomplete and type checking work across all generated interfaces.
|
|
75
75
|
|
|
76
|
+
## Documentation Comments
|
|
77
|
+
|
|
78
|
+
Generated `types.ts`, `operations.ts`, and `client.ts` include source documentation when present in WSDL/XSD.
|
|
79
|
+
|
|
80
|
+
`wsdl:documentation` on operations is emitted as method comments in `operations.ts` and `client.ts`. `xs:annotation/xs:documentation` on complex types, attributes, and elements is emitted as comments in `types.ts`.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
/**
|
|
84
|
+
* Thing payload.
|
|
85
|
+
*/
|
|
86
|
+
export interface Thing {
|
|
87
|
+
/**
|
|
88
|
+
* Display name.
|
|
89
|
+
*
|
|
90
|
+
* @xsd {"kind":"element","type":"xs:string","occurs":{"min":1,"max":1,"nillable":false}}
|
|
91
|
+
*/
|
|
92
|
+
name: string;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The existing `@xsd` metadata annotations are preserved for runtime marshaling and tooling.
|
|
97
|
+
|
|
98
|
+
OpenAPI generation also propagates these docs into `description` fields:
|
|
99
|
+
|
|
100
|
+
- Operation descriptions come from `wsdl:documentation` by default
|
|
101
|
+
- `--openapi-ops-file` `description` overrides operation documentation when provided
|
|
102
|
+
- Schema and property descriptions come from `xs:annotation/xs:documentation`
|
|
103
|
+
|
|
76
104
|
## Gateway Route Handlers
|
|
77
105
|
|
|
78
106
|
When generating a Fastify gateway (`--gateway-dir`), each SOAP operation gets a fully typed route handler. The handler imports the request type from the client, uses Fastify's `Body: T` generic for type inference, and wraps the SOAP response in a standard envelope.
|
|
@@ -99,9 +127,9 @@ export async function registerRoute_v1_weather_getcityforecastbyzip(fastify: Fas
|
|
|
99
127
|
|
|
100
128
|
Key features of the generated handlers:
|
|
101
129
|
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
130
|
+
- `Body: T` generic: Fastify infers `request.body` type from the route generic, enabling IDE autocomplete and compile-time checks
|
|
131
|
+
- JSON Schema validation: the `schema` import provides Fastify with request/response validation at runtime, before the handler runs
|
|
132
|
+
- Envelope wrapping: `buildSuccessEnvelope()` wraps the raw SOAP response in the standard `{ status, message, data, error }` envelope
|
|
105
133
|
|
|
106
134
|
See [Gateway Guide](gateway-guide.md) for the full architecture and [CLI Reference](cli-reference.md) for generation flags.
|
|
107
135
|
|
package/docs/testing.md
CHANGED
|
@@ -8,9 +8,9 @@ See [README](../README.md) for quick start and [CONTRIBUTING](../CONTRIBUTING.md
|
|
|
8
8
|
|
|
9
9
|
The project uses three layers of testing:
|
|
10
10
|
|
|
11
|
-
1.
|
|
12
|
-
2.
|
|
13
|
-
3.
|
|
11
|
+
1. Unit tests: Pure function tests for utilities, parsers, and type mapping
|
|
12
|
+
2. Snapshot tests: Baseline comparisons for all generated pipeline output
|
|
13
|
+
3. Integration tests: End-to-end gateway tests using Fastify's `inject()` with mock clients
|
|
14
14
|
|
|
15
15
|
All tests use [Vitest](https://vitest.dev/) and run in under 3 seconds.
|
|
16
16
|
|
|
@@ -34,12 +34,13 @@ npm run ci
|
|
|
34
34
|
|
|
35
35
|
Unit tests cover pure functions with no I/O or side effects:
|
|
36
36
|
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
37
|
+
- `tools.test.ts`: `pascal()`, `resolveQName()`, `explodePascal()`, `pascalToSnakeCase()`, `normalizeArray()`, `getChildrenWithLocalName()`, `getFirstWithLocalName()`
|
|
38
|
+
- `casing.test.ts`: `toPathSegment()` with kebab, asis, and lower styles
|
|
39
|
+
- `primitives.test.ts`: `xsdToTsPrimitive()` covering all XSD types (string-like, boolean, integers, decimals, floats, dates, any)
|
|
40
|
+
- `errors.test.ts`: `WsdlCompilationError` construction and `toUserMessage()` formatting
|
|
41
|
+
- `schema-alignment.test.ts`: Cross-validates TypeScript types, JSON schemas, and catalog.json for consistency
|
|
42
|
+
- `mock-data.test.ts`: `generateMockPrimitive()`, `generateMockData()`, `generateAllOperationMocks()` with cycle detection and array wrapping
|
|
43
|
+
- `wsdl-documentation.test.ts`: verifies doc flow to catalog, generated TS comments, and OpenAPI descriptions with override precedence
|
|
43
44
|
|
|
44
45
|
### Writing Unit Tests
|
|
45
46
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@techspokes/typescript-wsdl-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Generate type-safe TypeScript SOAP clients, OpenAPI 3.1 specs, and production-ready Fastify REST gateways from WSDL/XSD definitions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wsdl",
|
|
@@ -76,20 +76,20 @@
|
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@types/js-yaml": "^4.0.9",
|
|
79
|
-
"@types/node": "^25.
|
|
79
|
+
"@types/node": "^25.3.3",
|
|
80
80
|
"@types/yargs": "^17.0.35",
|
|
81
|
-
"fastify": "^5.7.
|
|
81
|
+
"fastify": "^5.7.4",
|
|
82
82
|
"fastify-plugin": "^5.1.0",
|
|
83
|
-
"rimraf": "^6.1.
|
|
83
|
+
"rimraf": "^6.1.3",
|
|
84
84
|
"tsx": "^4.21.0",
|
|
85
|
-
"typescript": "^5.9.
|
|
85
|
+
"typescript": "^5.9.3",
|
|
86
86
|
"vitest": "^4.0.18"
|
|
87
87
|
},
|
|
88
88
|
"dependencies": {
|
|
89
89
|
"@apidevtools/swagger-parser": "^12.1.0",
|
|
90
|
-
"fast-xml-parser": "^5.
|
|
90
|
+
"fast-xml-parser": "^5.4.2",
|
|
91
91
|
"js-yaml": "^4.1.1",
|
|
92
|
-
"soap": "^1.
|
|
92
|
+
"soap": "^1.7.1",
|
|
93
93
|
"yargs": "^18.0.0"
|
|
94
94
|
},
|
|
95
95
|
"funding": {
|