@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.
Files changed (41) hide show
  1. package/README.md +2 -1
  2. package/dist/app/generateApp.js +1 -1
  3. package/dist/client/generateClient.d.ts.map +1 -1
  4. package/dist/client/generateClient.js +9 -1
  5. package/dist/client/generateOperations.d.ts.map +1 -1
  6. package/dist/client/generateOperations.js +11 -1
  7. package/dist/client/generateTypes.d.ts.map +1 -1
  8. package/dist/client/generateTypes.js +49 -6
  9. package/dist/compiler/schemaCompiler.d.ts +8 -0
  10. package/dist/compiler/schemaCompiler.d.ts.map +1 -1
  11. package/dist/compiler/schemaCompiler.js +125 -12
  12. package/dist/gateway/generators.d.ts +2 -0
  13. package/dist/gateway/generators.d.ts.map +1 -1
  14. package/dist/gateway/generators.js +57 -23
  15. package/dist/gateway/helpers.d.ts +13 -0
  16. package/dist/gateway/helpers.d.ts.map +1 -1
  17. package/dist/gateway/helpers.js +58 -0
  18. package/dist/openapi/generatePaths.d.ts.map +1 -1
  19. package/dist/openapi/generatePaths.js +5 -1
  20. package/dist/openapi/generateSchemas.d.ts.map +1 -1
  21. package/dist/openapi/generateSchemas.js +29 -5
  22. package/dist/pipeline.d.ts +1 -0
  23. package/dist/pipeline.d.ts.map +1 -1
  24. package/dist/pipeline.js +1 -0
  25. package/dist/test/generateTests.d.ts +1 -0
  26. package/dist/test/generateTests.d.ts.map +1 -1
  27. package/dist/test/generateTests.js +9 -4
  28. package/dist/test/generators.d.ts +12 -7
  29. package/dist/test/generators.d.ts.map +1 -1
  30. package/dist/test/generators.js +41 -37
  31. package/dist/test/mockData.d.ts +23 -10
  32. package/dist/test/mockData.d.ts.map +1 -1
  33. package/dist/test/mockData.js +44 -4
  34. package/dist/util/catalogMeta.d.ts +33 -0
  35. package/dist/util/catalogMeta.d.ts.map +1 -0
  36. package/dist/util/catalogMeta.js +95 -0
  37. package/docs/architecture.md +2 -0
  38. package/docs/concepts.md +2 -0
  39. package/docs/generated-code.md +31 -3
  40. package/docs/testing.md +10 -9
  41. package/package.json +7 -7
@@ -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 { generateAllOperationMocks } from "./mockData.js";
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 { dirname } from "node:path";
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 catalog - Compiled catalog for mock data generation
46
+ * @param mocks - Pre-computed mock data map
46
47
  */
47
- export function emitMockClientHelper(testDir, clientDir, importsMode, clientMeta, operations, catalog) {
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 catalog - Compiled catalog for mock data generation
144
+ * @param mocks - Pre-computed mock data map
145
145
  */
146
- export function emitRoutesTest(testDir, importsMode, operations, catalog) {
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 catalog - Compiled catalog for mock data generation
194
+ * @param mocks - Pre-computed mock data map
195
195
  */
196
- export function emitErrorsTest(testDir, importsMode, operations, catalog) {
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
- export function emitEnvelopeTest(testDir, importsMode, operations, catalog) {
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 wrappers = detectArrayWrappersFromCatalog(catalog);
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
- }
@@ -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":"AAAA;;;;;;;;GAQG;AAEH;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE;QACL,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;CACJ;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAiCjG;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,CAoCzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,eAAe,GACvB,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,CAkBtF"}
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"}
@@ -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
- if (!childTypes || Object.keys(childTypes).length === 0) {
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
- const request = op.inputTypeName
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
+ }
@@ -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 |
@@ -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
- - **`Body: T` generic** Fastify infers `request.body` type from the route generic, enabling IDE autocomplete and compile-time checks
103
- - **JSON Schema validation** the `schema` import provides Fastify with request/response validation at runtime, before the handler runs
104
- - **Envelope wrapping** `buildSuccessEnvelope()` wraps the raw SOAP response in the standard `{ status, message, data, error }` envelope
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. **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
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
- - **`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
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.11.5",
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.2.0",
79
+ "@types/node": "^25.3.3",
80
80
  "@types/yargs": "^17.0.35",
81
- "fastify": "^5.7.0",
81
+ "fastify": "^5.7.4",
82
82
  "fastify-plugin": "^5.1.0",
83
- "rimraf": "^6.1.0",
83
+ "rimraf": "^6.1.3",
84
84
  "tsx": "^4.21.0",
85
- "typescript": "^5.9.0",
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.3.0",
90
+ "fast-xml-parser": "^5.4.2",
91
91
  "js-yaml": "^4.1.1",
92
- "soap": "^1.6.0",
92
+ "soap": "^1.7.1",
93
93
  "yargs": "^18.0.0"
94
94
  },
95
95
  "funding": {