@techspokes/typescript-wsdl-client 0.10.2 → 0.11.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 (48) hide show
  1. package/README.md +28 -2
  2. package/dist/app/generateApp.d.ts +11 -5
  3. package/dist/app/generateApp.d.ts.map +1 -1
  4. package/dist/app/generateApp.js +262 -157
  5. package/dist/cli.js +67 -9
  6. package/dist/client/generateOperations.d.ts +13 -0
  7. package/dist/client/generateOperations.d.ts.map +1 -0
  8. package/dist/client/generateOperations.js +71 -0
  9. package/dist/compiler/schemaCompiler.d.ts.map +1 -1
  10. package/dist/compiler/schemaCompiler.js +15 -1
  11. package/dist/gateway/generateGateway.d.ts +1 -0
  12. package/dist/gateway/generateGateway.d.ts.map +1 -1
  13. package/dist/gateway/generateGateway.js +4 -2
  14. package/dist/gateway/generators.d.ts +2 -15
  15. package/dist/gateway/generators.d.ts.map +1 -1
  16. package/dist/gateway/generators.js +111 -27
  17. package/dist/gateway/helpers.d.ts +4 -2
  18. package/dist/gateway/helpers.d.ts.map +1 -1
  19. package/dist/gateway/helpers.js +4 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +3 -0
  22. package/dist/loader/wsdlLoader.d.ts.map +1 -1
  23. package/dist/loader/wsdlLoader.js +30 -4
  24. package/dist/openapi/generateOpenAPI.d.ts +1 -0
  25. package/dist/openapi/generateOpenAPI.d.ts.map +1 -1
  26. package/dist/openapi/generateOpenAPI.js +1 -0
  27. package/dist/openapi/generateSchemas.d.ts +1 -0
  28. package/dist/openapi/generateSchemas.d.ts.map +1 -1
  29. package/dist/openapi/generateSchemas.js +4 -3
  30. package/dist/pipeline.d.ts +4 -0
  31. package/dist/pipeline.d.ts.map +1 -1
  32. package/dist/pipeline.js +10 -1
  33. package/dist/util/builder.d.ts.map +1 -1
  34. package/dist/util/builder.js +1 -0
  35. package/dist/util/cli.d.ts +3 -2
  36. package/dist/util/cli.d.ts.map +1 -1
  37. package/dist/util/cli.js +14 -4
  38. package/dist/util/errors.d.ts +37 -0
  39. package/dist/util/errors.d.ts.map +1 -0
  40. package/dist/util/errors.js +37 -0
  41. package/docs/README.md +1 -0
  42. package/docs/architecture.md +1 -1
  43. package/docs/cli-reference.md +46 -14
  44. package/docs/concepts.md +29 -2
  45. package/docs/gateway-guide.md +36 -2
  46. package/docs/generated-code.md +56 -0
  47. package/docs/testing.md +193 -0
  48. package/package.json +19 -13
@@ -63,17 +63,18 @@ Route handlers call the SOAP client automatically:
63
63
 
64
64
  ```typescript
65
65
  import type { FastifyInstance } from "fastify";
66
+ import type { GetCityForecastByZIP } from "../../client/types.js";
66
67
  import schema from "../schemas/operations/getcityforecastbyzip.json" with { type: "json" };
67
68
  import { buildSuccessEnvelope } from "../runtime.js";
68
69
 
69
70
  export async function registerRoute_v1_weather_getcityforecastbyzip(fastify: FastifyInstance) {
70
- fastify.route({
71
+ fastify.route<{ Body: GetCityForecastByZIP }>({
71
72
  method: "POST",
72
73
  url: "/get-city-forecast-by-zip",
73
74
  schema,
74
75
  handler: async (request) => {
75
76
  const client = fastify.weatherClient;
76
- const result = await client.GetCityForecastByZIP(request.body);
77
+ const result = await client.GetCityForecastByZIP(request.body as GetCityForecastByZIP);
77
78
  return buildSuccessEnvelope(result.response);
78
79
  },
79
80
  });
@@ -117,6 +118,39 @@ All generated JSON Schemas use deterministic URN identifiers:
117
118
 
118
119
  Example: `urn:services:weather:v1:schemas:models:getcityweatherbyzipresponse`
119
120
 
121
+ ## Multi-Service Setup
122
+
123
+ When integrating multiple SOAP services, each service gets its own client, gateway, and OpenAPI spec. Register each in its own Fastify encapsulation scope to prevent decorator collisions:
124
+
125
+ ```typescript
126
+ import Fastify from "fastify";
127
+ import weatherPlugin from "./generated/weather/gateway/plugin.js";
128
+ import inventoryPlugin from "./generated/inventory/gateway/plugin.js";
129
+ import { Weather } from "./generated/weather/client/client.js";
130
+ import { Inventory } from "./generated/inventory/client/client.js";
131
+
132
+ const app = Fastify({ logger: true });
133
+
134
+ // Each plugin gets its own scope to isolate decorators
135
+ await app.register(async (scope) => {
136
+ await scope.register(weatherPlugin, {
137
+ client: new Weather({ source: "https://example.com/weather?wsdl" }),
138
+ prefix: "/api/weather",
139
+ });
140
+ });
141
+
142
+ await app.register(async (scope) => {
143
+ await scope.register(inventoryPlugin, {
144
+ client: new Inventory({ source: "https://example.com/inventory?wsdl" }),
145
+ prefix: "/api/inventory",
146
+ });
147
+ });
148
+
149
+ await app.listen({ port: 3000 });
150
+ ```
151
+
152
+ See [`examples/fastify-gateway/`](../examples/fastify-gateway/) for a complete example.
153
+
120
154
  ## Contract Assumptions
121
155
 
122
156
  - All request/response bodies must use $ref to components.schemas
@@ -72,3 +72,59 @@ result.GetCityWeatherByZIPResult.Temperature;
72
72
  ```
73
73
 
74
74
  Autocomplete and type checking work across all generated interfaces.
75
+
76
+ ## Gateway Route Handlers
77
+
78
+ 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.
79
+
80
+ ```typescript
81
+ import type { FastifyInstance } from "fastify";
82
+ import type { GetCityForecastByZIP } from "../../client/types.js";
83
+ import schema from "../schemas/operations/getcityforecastbyzip.json" with { type: "json" };
84
+ import { buildSuccessEnvelope } from "../runtime.js";
85
+
86
+ export async function registerRoute_v1_weather_getcityforecastbyzip(fastify: FastifyInstance) {
87
+ fastify.route<{ Body: GetCityForecastByZIP }>({
88
+ method: "POST",
89
+ url: "/get-city-forecast-by-zip",
90
+ schema,
91
+ handler: async (request) => {
92
+ const client = fastify.weatherClient;
93
+ const result = await client.GetCityForecastByZIP(request.body as GetCityForecastByZIP);
94
+ return buildSuccessEnvelope(result.response);
95
+ },
96
+ });
97
+ }
98
+ ```
99
+
100
+ Key features of the generated handlers:
101
+
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
105
+
106
+ See [Gateway Guide](gateway-guide.md) for the full architecture and [CLI Reference](cli-reference.md) for generation flags.
107
+
108
+ ## Operations Interface
109
+
110
+ The generated `operations.ts` exports a typed interface (`{ServiceName}Operations`) that mirrors the concrete client class methods. Use it for dependency injection and testing:
111
+
112
+ ```typescript
113
+ import type { WeatherOperations } from "./client/operations.js";
114
+
115
+ const mock: WeatherOperations = {
116
+ GetCityWeatherByZIP: async (args) => ({
117
+ response: { GetCityWeatherByZIPResult: { Success: true } },
118
+ headers: {},
119
+ }),
120
+ // ...other operations
121
+ };
122
+ ```
123
+
124
+ The gateway plugin accepts any `WeatherOperations` implementation, so you can pass the mock directly:
125
+
126
+ ```typescript
127
+ app.register(weatherGateway, { client: mock });
128
+ ```
129
+
130
+ See [Testing Guide](testing.md) for full integration test patterns.
@@ -0,0 +1,193 @@
1
+ # Testing Guide
2
+
3
+ Guide to running and writing tests for wsdl-tsc development and for testing generated code in consumer projects.
4
+
5
+ See [README](../README.md) for quick start and [CONTRIBUTING](../CONTRIBUTING.md) for development setup.
6
+
7
+ ## Test Architecture
8
+
9
+ The project uses three layers of testing:
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
14
+
15
+ All tests use [Vitest](https://vitest.dev/) and run in under 3 seconds.
16
+
17
+ ## Running Tests
18
+
19
+ ```bash
20
+ npm test # All Vitest tests
21
+ npm run test:unit # Unit tests only
22
+ npm run test:snap # Snapshot tests only
23
+ npm run test:integration # Integration tests only
24
+ npm run test:watch # Watch mode for development
25
+ ```
26
+
27
+ For the full CI pipeline including smoke tests:
28
+
29
+ ```bash
30
+ npm run ci
31
+ ```
32
+
33
+ ## Unit Tests
34
+
35
+ Unit tests cover pure functions with no I/O or side effects:
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
+
43
+ ### Writing Unit Tests
44
+
45
+ ```typescript
46
+ import { describe, it, expect } from "vitest";
47
+ import { pascal } from "../../src/util/tools.js";
48
+
49
+ describe("pascal", () => {
50
+ it("converts kebab-case", () => {
51
+ expect(pascal("get-weather")).toBe("GetWeather");
52
+ });
53
+ });
54
+ ```
55
+
56
+ ## Snapshot Tests
57
+
58
+ Snapshot tests capture the complete output of the pipeline as baselines. When a generator change intentionally alters output, the snapshot diff shows exactly what changed.
59
+
60
+ ### How It Works
61
+
62
+ 1. The pipeline runs against `examples/minimal/weather.wsdl` into a temp directory
63
+ 2. Each generated file is read and compared against the stored snapshot
64
+ 3. A file inventory snapshot detects added or removed files
65
+
66
+ ### Updating Snapshots
67
+
68
+ ```bash
69
+ npx vitest run test/snapshot -u
70
+ ```
71
+
72
+ Always review the diff before committing updated snapshots.
73
+
74
+ ### What's Covered
75
+
76
+ - Client output: `client.ts`, `types.ts`, `utils.ts`, `operations.ts`, `catalog.json`
77
+ - OpenAPI: `openapi.json`
78
+ - Gateway core: `plugin.ts`, `routes.ts`, `schemas.ts`, `runtime.ts`, `_typecheck.ts`
79
+ - Gateway routes: one handler per WSDL operation
80
+ - Gateway schemas: all model and operation JSON schema files
81
+ - File inventory: complete listing of generated files
82
+
83
+ ## Integration Tests
84
+
85
+ Integration tests verify the generated gateway works end-to-end by:
86
+
87
+ 1. Running the pipeline in `beforeAll` to generate gateway code
88
+ 2. Dynamically importing the generated plugin
89
+ 3. Creating a Fastify instance with the plugin and a mock client
90
+ 4. Using `fastify.inject()` to send HTTP requests and verify responses
91
+
92
+ ### Mock Client Pattern
93
+
94
+ The generated `operations.ts` provides a typed interface for creating test doubles:
95
+
96
+ ```typescript
97
+ import type { WeatherOperations } from "../client/operations.js";
98
+
99
+ function createMockClient(): WeatherOperations {
100
+ return {
101
+ GetCityWeatherByZIP: async (args) => ({
102
+ response: {
103
+ GetCityWeatherByZIPResult: {
104
+ Success: true,
105
+ ResponseText: "City Found",
106
+ State: "NY",
107
+ City: "New York",
108
+ Temperature: "72",
109
+ },
110
+ },
111
+ headers: {},
112
+ }),
113
+ GetCityForecastByZIP: async (args) => ({
114
+ response: {
115
+ GetCityForecastByZIPResult: {
116
+ Success: true,
117
+ ResponseText: "Forecast Found",
118
+ // Use SOAP wrapper shape — unwrapArrayWrappers() handles conversion
119
+ ForecastResult: { Forecast: [] },
120
+ },
121
+ },
122
+ headers: {},
123
+ }),
124
+ GetWeatherInformation: async (args) => ({
125
+ response: {
126
+ // Use SOAP wrapper shape — unwrapArrayWrappers() handles conversion
127
+ GetWeatherInformationResult: { WeatherDescription: [] },
128
+ },
129
+ headers: {},
130
+ }),
131
+ };
132
+ }
133
+ ```
134
+
135
+ Each method returns the same `{ response, headers }` shape as the real SOAP client. Use the wrapper object structure matching TypeScript types — the generated `unwrapArrayWrappers()` function handles conversion to the flat array shape expected by JSON schemas.
136
+
137
+ ### Using the Mock with Fastify
138
+
139
+ ```typescript
140
+ import Fastify from "fastify";
141
+
142
+ const app = Fastify();
143
+ await app.register(weatherGateway, {
144
+ client: createMockClient(),
145
+ prefix: "/v1/weather",
146
+ });
147
+ await app.ready();
148
+
149
+ const res = await app.inject({
150
+ method: "POST",
151
+ url: "/v1/weather/get-city-weather-by-zip",
152
+ payload: { ZIP: "10001" },
153
+ });
154
+
155
+ expect(res.statusCode).toBe(200);
156
+ expect(res.json().status).toBe("SUCCESS");
157
+ ```
158
+
159
+ ### Dynamic Import of Generated Code
160
+
161
+ Integration tests dynamically import generated `.ts` files from temp directories. This works because Vitest's Vite module resolution handles TypeScript imports, JSON import attributes, and bare specifiers from the project's `node_modules`:
162
+
163
+ ```typescript
164
+ import { pathToFileURL } from "node:url";
165
+
166
+ const pluginModule = await import(pathToFileURL(join(outDir, "gateway", "plugin.ts")).href);
167
+ ```
168
+
169
+ ## Known Issues
170
+
171
+ ### ArrayOf* Schema-Type Mismatch (Resolved)
172
+
173
+ JSON schemas flatten SOAP `ArrayOf*` wrapper types to plain `type: "array"` (when `--openapi-flatten-array-wrappers` is `true`, the default), while TypeScript types preserve the wrapper structure (e.g., `ArrayOfForecast = { Forecast?: Forecast[] }`).
174
+
175
+ This mismatch is resolved by the generated `unwrapArrayWrappers()` function in `runtime.ts`. Route handlers call it automatically to strip wrapper objects before Fastify serialization. Mock clients should return the real SOAP wrapper structure — the unwrap function handles the conversion.
176
+
177
+ When `--openapi-flatten-array-wrappers false` is used, ArrayOf* types are emitted as `type: "object"` and no unwrap function is generated. In this mode, mock data should use the wrapper object shape matching both the TypeScript types and the JSON schemas.
178
+
179
+ ### Error Details Serialization
180
+
181
+ The `classifyError()` function puts `err.message` (a string) in the `details` field for connection and timeout errors, but the error JSON schema defines `details` as `object | null`. This causes Fastify serialization failures for 503/504 error responses. Test error classification directly via `classifyError()` rather than through Fastify's `inject()` for these error types.
182
+
183
+ ## For Consumer Projects
184
+
185
+ If you're using wsdl-tsc as a dependency and want to test your integration:
186
+
187
+ 1. Generate code with `npx wsdl-tsc pipeline`
188
+ 2. Import the operations interface from `operations.ts`
189
+ 3. Create a mock client implementing the interface
190
+ 4. Register the generated gateway plugin with your mock client
191
+ 5. Use Fastify's `inject()` to test routes without a running server
192
+
193
+ The operations interface is the recommended seam for dependency injection and testing. It's a pure TypeScript interface with no runtime dependencies on the `soap` package.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techspokes/typescript-wsdl-client",
3
- "version": "0.10.2",
3
+ "version": "0.11.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",
@@ -58,32 +58,38 @@
58
58
  "clean:tmp": "rimraf tmp",
59
59
  "build": "tsc -p tsconfig.json",
60
60
  "typecheck": "tsc --noEmit",
61
- "test": "npm run typecheck && npm run smoke:pipeline",
61
+ "test": "vitest run",
62
+ "test:unit": "vitest run test/unit",
63
+ "test:snap": "vitest run test/snapshot",
64
+ "test:integration": "vitest run test/integration",
65
+ "test:watch": "vitest",
62
66
  "prepublishOnly": "npm run clean && npm run build",
63
67
  "smoke:reset": "npm run clean:tmp",
64
68
  "smoke:compile": "npm run smoke:reset && tsx src/cli.ts compile --wsdl-source examples/minimal/weather.wsdl --catalog-file tmp/catalog.json",
65
69
  "smoke:client": "npm run smoke:reset && tsx src/cli.ts client --wsdl-source examples/minimal/weather.wsdl --client-dir tmp/client && tsc -p tsconfig.smoke.json",
66
70
  "smoke:openapi": "npm run smoke:reset && tsx src/cli.ts openapi --wsdl-source examples/minimal/weather.wsdl --openapi-file tmp/openapi.json --openapi-format json && tsc -p tsconfig.smoke.json",
67
71
  "smoke:gateway": "npm run smoke:reset && tsx src/cli.ts client --wsdl-source examples/minimal/weather.wsdl --client-dir tmp/client && tsx src/cli.ts openapi --catalog-file tmp/client/catalog.json --openapi-file tmp/openapi.json --openapi-format json && tsx src/cli.ts gateway --openapi-file tmp/openapi.json --client-dir tmp/client --gateway-dir tmp/gateway --gateway-service-name weather --gateway-version-prefix v1 && tsc -p tsconfig.smoke.json",
68
- "smoke:pipeline": "npm run smoke:reset && tsx src/cli.ts pipeline --wsdl-source examples/minimal/weather.wsdl --client-dir tmp/client --openapi-file tmp/openapi.json --gateway-dir tmp/gateway --gateway-service-name weather --gateway-version-prefix v1 --openapi-format json --openapi-servers https://example.com/api --generate-app && tsc -p tsconfig.smoke.json",
69
- "smoke:app": "npm run smoke:reset && tsx src/cli.ts pipeline --wsdl-source examples/minimal/weather.wsdl --client-dir tmp/client --openapi-file tmp/openapi.json --gateway-dir tmp/gateway --gateway-service-name weather --gateway-version-prefix v1 --openapi-format json --openapi-servers https://example.com/api && tsx src/cli.ts app --client-dir tmp/client --gateway-dir tmp/gateway --openapi-file tmp/openapi.json --app-dir tmp/app && tsc -p tsconfig.smoke.json",
70
- "ci": "npm run clean && npm run build && npm run typecheck && npm run smoke:pipeline"
72
+ "smoke:pipeline": "npm run smoke:reset && tsx src/cli.ts pipeline --wsdl-source examples/minimal/weather.wsdl --client-dir tmp/client --openapi-file tmp/openapi.json --gateway-dir tmp/gateway --gateway-service-name weather --gateway-version-prefix v1 --openapi-format json --init-app && tsc -p tsconfig.smoke.json",
73
+ "smoke:app": "npm run smoke:reset && tsx src/cli.ts pipeline --wsdl-source examples/minimal/weather.wsdl --client-dir tmp/client --openapi-file tmp/openapi.json --gateway-dir tmp/gateway --gateway-service-name weather --gateway-version-prefix v1 --openapi-format json --openapi-servers https://example.com/api && tsx src/cli.ts app --client-dir tmp/client --gateway-dir tmp/gateway --openapi-file tmp/openapi.json --app-dir tmp/app --port 8080 && tsc -p tsconfig.smoke.json",
74
+ "ci": "npm run clean && npm run build && npm run typecheck && vitest run && npm run smoke:pipeline",
75
+ "examples:regenerate": "tsx src/cli.ts pipeline --wsdl-source examples/minimal/weather.wsdl --client-dir examples/generated-output/client --openapi-file examples/generated-output/openapi.json --gateway-dir examples/generated-output/gateway --gateway-service-name weather --gateway-version-prefix v1 --openapi-format json"
71
76
  },
72
77
  "devDependencies": {
73
78
  "@types/js-yaml": "^4.0.9",
74
- "@types/node": "^25.0.2",
75
- "@types/yargs": "^17.0.33",
76
- "fastify": "^5.2.0",
79
+ "@types/node": "^25.2.0",
80
+ "@types/yargs": "^17.0.35",
81
+ "fastify": "^5.7.0",
77
82
  "fastify-plugin": "^5.1.0",
78
- "rimraf": "^6.0.0",
79
- "tsx": "^4.20.0",
80
- "typescript": "^5.6.3"
83
+ "rimraf": "^6.1.0",
84
+ "tsx": "^4.21.0",
85
+ "typescript": "^5.9.0",
86
+ "vitest": "^4.0.18"
81
87
  },
82
88
  "dependencies": {
83
89
  "@apidevtools/swagger-parser": "^12.1.0",
84
- "fast-xml-parser": "^5.2.5",
90
+ "fast-xml-parser": "^5.3.0",
85
91
  "js-yaml": "^4.1.1",
86
- "soap": "^1.3.0",
92
+ "soap": "^1.6.0",
87
93
  "yargs": "^18.0.0"
88
94
  },
89
95
  "funding": {