@techspokes/typescript-wsdl-client 0.16.1 → 0.17.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 +5 -0
- package/dist/app/generateApp.d.ts.map +1 -1
- package/dist/app/generateApp.js +1 -0
- package/dist/cli.js +46 -2
- package/dist/client/generateClient.d.ts.map +1 -1
- package/dist/client/generateClient.js +62 -6
- package/dist/client/generateOperations.d.ts.map +1 -1
- package/dist/client/generateOperations.js +27 -4
- package/dist/compiler/schemaCompiler.d.ts +40 -11
- package/dist/compiler/schemaCompiler.d.ts.map +1 -1
- package/dist/compiler/schemaCompiler.js +81 -6
- package/dist/compiler/shapeResolver.d.ts +18 -0
- package/dist/compiler/shapeResolver.d.ts.map +1 -0
- package/dist/compiler/shapeResolver.js +280 -0
- package/dist/gateway/generateGateway.d.ts.map +1 -1
- package/dist/gateway/generateGateway.js +2 -1
- package/dist/gateway/generators.d.ts +13 -1
- package/dist/gateway/generators.d.ts.map +1 -1
- package/dist/gateway/generators.js +98 -13
- package/dist/gateway/helpers.d.ts +16 -0
- package/dist/gateway/helpers.d.ts.map +1 -1
- package/dist/gateway/helpers.js +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -1
- package/dist/openapi/generateOpenAPI.d.ts.map +1 -1
- package/dist/openapi/generateOpenAPI.js +28 -0
- package/dist/pipeline.d.ts +13 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +17 -1
- package/dist/runtime/ndjson.d.ts +24 -0
- package/dist/runtime/ndjson.d.ts.map +1 -0
- package/dist/runtime/ndjson.js +30 -0
- package/dist/runtime/streamXml.d.ts +45 -0
- package/dist/runtime/streamXml.d.ts.map +1 -0
- package/dist/runtime/streamXml.js +212 -0
- package/dist/test/generators.d.ts.map +1 -1
- package/dist/test/generators.js +50 -0
- package/dist/test/mockData.d.ts +6 -0
- package/dist/test/mockData.d.ts.map +1 -1
- package/dist/test/mockData.js +6 -2
- package/dist/util/cli.d.ts.map +1 -1
- package/dist/util/cli.js +3 -1
- package/dist/util/runtimeSource.d.ts +2 -0
- package/dist/util/runtimeSource.d.ts.map +1 -0
- package/dist/util/runtimeSource.js +38 -0
- package/dist/util/streamConfig.d.ts +59 -0
- package/dist/util/streamConfig.d.ts.map +1 -0
- package/dist/util/streamConfig.js +230 -0
- package/docs/README.md +1 -0
- package/docs/api-reference.md +146 -0
- package/docs/architecture.md +27 -5
- package/docs/cli-reference.md +30 -0
- package/docs/concepts.md +51 -0
- package/docs/configuration.md +40 -0
- package/docs/decisions/002-streamable-responses.md +308 -0
- package/docs/gateway-guide.md +37 -0
- package/docs/generated-code.md +21 -0
- package/docs/migration-playbook.md +33 -0
- package/docs/migration.md +31 -6
- package/docs/output-anatomy.md +49 -0
- package/docs/production.md +32 -0
- package/docs/start-here.md +33 -0
- package/docs/supported-patterns.md +2 -0
- package/docs/testing.md +14 -0
- package/docs/troubleshooting.md +18 -0
- package/package.json +5 -2
- package/src/runtime/clientStreamMethods.tpl.txt +183 -0
- package/src/runtime/ndjson.ts +32 -0
- package/src/runtime/operationsStreamHelper.tpl.txt +13 -0
- package/src/runtime/streamXml.ts +293 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-operation stream metadata, normalized form. Exported so that the
|
|
3
|
+
* compiler, emitters, and gateway all consume the same shape.
|
|
4
|
+
*/
|
|
5
|
+
export interface OperationStreamMetadata {
|
|
6
|
+
mode: "stream";
|
|
7
|
+
format: "ndjson" | "json-array";
|
|
8
|
+
mediaType: string;
|
|
9
|
+
recordPath: string[];
|
|
10
|
+
recordTypeName: string;
|
|
11
|
+
shapeCatalogName?: string;
|
|
12
|
+
sourceOutputTypeName?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Reference to a companion catalog that supplies record shapes. Exactly one
|
|
16
|
+
* of `wsdlSource` or `catalogFile` must be set.
|
|
17
|
+
*/
|
|
18
|
+
export interface ShapeCatalogRef {
|
|
19
|
+
wsdlSource?: string;
|
|
20
|
+
catalogFile?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parsed and normalized stream configuration.
|
|
24
|
+
*/
|
|
25
|
+
export interface StreamConfig {
|
|
26
|
+
shapeCatalogs: Record<string, ShapeCatalogRef>;
|
|
27
|
+
operations: Record<string, OperationStreamMetadata>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Structured error thrown when a stream configuration cannot be parsed.
|
|
31
|
+
* The CLI error handler prints `.toUserMessage()` verbatim.
|
|
32
|
+
*/
|
|
33
|
+
export declare class StreamConfigError extends Error {
|
|
34
|
+
readonly context: {
|
|
35
|
+
file?: string;
|
|
36
|
+
pointer?: string;
|
|
37
|
+
suggestion?: string;
|
|
38
|
+
};
|
|
39
|
+
readonly name = "StreamConfigError";
|
|
40
|
+
constructor(message: string, context?: {
|
|
41
|
+
file?: string;
|
|
42
|
+
pointer?: string;
|
|
43
|
+
suggestion?: string;
|
|
44
|
+
});
|
|
45
|
+
toUserMessage(): string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Parse a stream configuration from an in-memory JSON value. Used directly
|
|
49
|
+
* by tests; the file-based variant below wraps this.
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseStreamConfig(raw: unknown, opts?: {
|
|
52
|
+
file?: string;
|
|
53
|
+
}): StreamConfig;
|
|
54
|
+
/**
|
|
55
|
+
* Load and parse a stream configuration file. Relative paths are resolved
|
|
56
|
+
* against the caller's cwd.
|
|
57
|
+
*/
|
|
58
|
+
export declare function loadStreamConfigFile(filePath: string): StreamConfig;
|
|
59
|
+
//# sourceMappingURL=streamConfig.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streamConfig.d.ts","sourceRoot":"","sources":["../../src/util/streamConfig.ts"],"names":[],"mappings":"AAeA;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,QAAQ,GAAG,YAAY,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAG1B,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC/C,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC;CACrD;AAED;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;aAKxB,OAAO,EAAE;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAC;IAJjF,SAAkB,IAAI,uBAAuB;gBAG3C,OAAO,EAAE,MAAM,EACC,OAAO,GAAE;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAM;IAKtF,aAAa,IAAI,MAAM;CAOxB;AASD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,GAAE;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAM,GAAG,YAAY,CAwBxF;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,CAqBnE"}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stream-configuration parsing and validation.
|
|
3
|
+
*
|
|
4
|
+
* Backs the `--stream-config <file>` CLI flag on every generation command.
|
|
5
|
+
* The file is a small JSON document that marks selected WSDL operations as
|
|
6
|
+
* streamable and optionally declares companion catalogs that supply record
|
|
7
|
+
* shapes not present in the main WSDL. See `docs/decisions/002-streamable-responses.md`.
|
|
8
|
+
*
|
|
9
|
+
* This module is parser-only: it reads the JSON, validates it, and returns a
|
|
10
|
+
* normalized shape. It never loads companion catalogs or compiles WSDLs —
|
|
11
|
+
* that is the shape-resolver's job in phase-2.
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
/**
|
|
16
|
+
* Structured error thrown when a stream configuration cannot be parsed.
|
|
17
|
+
* The CLI error handler prints `.toUserMessage()` verbatim.
|
|
18
|
+
*/
|
|
19
|
+
export class StreamConfigError extends Error {
|
|
20
|
+
context;
|
|
21
|
+
name = "StreamConfigError";
|
|
22
|
+
constructor(message, context = {}) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.context = context;
|
|
25
|
+
}
|
|
26
|
+
toUserMessage() {
|
|
27
|
+
const parts = [this.message];
|
|
28
|
+
if (this.context.pointer)
|
|
29
|
+
parts.push(` At: ${this.context.pointer}`);
|
|
30
|
+
if (this.context.file)
|
|
31
|
+
parts.push(` File: ${this.context.file}`);
|
|
32
|
+
if (this.context.suggestion)
|
|
33
|
+
parts.push(` Suggestion: ${this.context.suggestion}`);
|
|
34
|
+
return parts.join("\n");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const SUPPORTED_FORMATS = new Set(["ndjson", "json-array"]);
|
|
38
|
+
const DEFAULT_MEDIA_TYPE_BY_FORMAT = {
|
|
39
|
+
"ndjson": "application/x-ndjson",
|
|
40
|
+
"json-array": "application/json",
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Parse a stream configuration from an in-memory JSON value. Used directly
|
|
44
|
+
* by tests; the file-based variant below wraps this.
|
|
45
|
+
*/
|
|
46
|
+
export function parseStreamConfig(raw, opts = {}) {
|
|
47
|
+
const file = opts.file;
|
|
48
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
49
|
+
throw new StreamConfigError("Stream config must be a JSON object.", {
|
|
50
|
+
file,
|
|
51
|
+
pointer: "$",
|
|
52
|
+
suggestion: `Expected an object with "shapeCatalogs" and "operations" keys.`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const obj = raw;
|
|
56
|
+
const shapeCatalogs = parseShapeCatalogs(obj["shapeCatalogs"], file);
|
|
57
|
+
const operations = parseOperations(obj["operations"], shapeCatalogs, file);
|
|
58
|
+
if (Object.keys(operations).length === 0) {
|
|
59
|
+
throw new StreamConfigError(`Stream config must declare at least one operation.`, {
|
|
60
|
+
file,
|
|
61
|
+
pointer: "$.operations",
|
|
62
|
+
suggestion: `Add an entry under "operations" keyed by the WSDL operation name.`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return { shapeCatalogs, operations };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Load and parse a stream configuration file. Relative paths are resolved
|
|
69
|
+
* against the caller's cwd.
|
|
70
|
+
*/
|
|
71
|
+
export function loadStreamConfigFile(filePath) {
|
|
72
|
+
const abs = path.resolve(filePath);
|
|
73
|
+
let text;
|
|
74
|
+
try {
|
|
75
|
+
text = fs.readFileSync(abs, "utf-8");
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
throw new StreamConfigError(`Failed to read stream config file: ${err.message}`, { file: abs, suggestion: "Check that --stream-config points to an existing, readable file." });
|
|
79
|
+
}
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = JSON.parse(text);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
throw new StreamConfigError(`Stream config file is not valid JSON: ${err.message}`, { file: abs, suggestion: "Fix the JSON syntax and retry." });
|
|
86
|
+
}
|
|
87
|
+
return parseStreamConfig(parsed, { file: abs });
|
|
88
|
+
}
|
|
89
|
+
function parseShapeCatalogs(raw, file) {
|
|
90
|
+
if (raw === undefined)
|
|
91
|
+
return {};
|
|
92
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
93
|
+
throw new StreamConfigError(`"shapeCatalogs" must be an object.`, {
|
|
94
|
+
file,
|
|
95
|
+
pointer: "$.shapeCatalogs",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const out = {};
|
|
99
|
+
for (const [name, entry] of Object.entries(raw)) {
|
|
100
|
+
if (!name) {
|
|
101
|
+
throw new StreamConfigError(`"shapeCatalogs" keys must be non-empty strings.`, {
|
|
102
|
+
file,
|
|
103
|
+
pointer: `$.shapeCatalogs`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
107
|
+
throw new StreamConfigError(`Shape catalog "${name}" must be an object.`, {
|
|
108
|
+
file,
|
|
109
|
+
pointer: `$.shapeCatalogs.${name}`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const e = entry;
|
|
113
|
+
const wsdlSource = e["wsdlSource"];
|
|
114
|
+
const catalogFile = e["catalogFile"];
|
|
115
|
+
if (wsdlSource !== undefined && catalogFile !== undefined) {
|
|
116
|
+
throw new StreamConfigError(`Shape catalog "${name}" must set exactly one of "wsdlSource" or "catalogFile", not both.`, { file, pointer: `$.shapeCatalogs.${name}` });
|
|
117
|
+
}
|
|
118
|
+
if (wsdlSource === undefined && catalogFile === undefined) {
|
|
119
|
+
throw new StreamConfigError(`Shape catalog "${name}" must set one of "wsdlSource" or "catalogFile".`, { file, pointer: `$.shapeCatalogs.${name}` });
|
|
120
|
+
}
|
|
121
|
+
if (wsdlSource !== undefined && (typeof wsdlSource !== "string" || !wsdlSource)) {
|
|
122
|
+
throw new StreamConfigError(`Shape catalog "${name}".wsdlSource must be a non-empty string.`, { file, pointer: `$.shapeCatalogs.${name}.wsdlSource` });
|
|
123
|
+
}
|
|
124
|
+
if (catalogFile !== undefined && (typeof catalogFile !== "string" || !catalogFile)) {
|
|
125
|
+
throw new StreamConfigError(`Shape catalog "${name}".catalogFile must be a non-empty string.`, { file, pointer: `$.shapeCatalogs.${name}.catalogFile` });
|
|
126
|
+
}
|
|
127
|
+
out[name] = {
|
|
128
|
+
wsdlSource: wsdlSource,
|
|
129
|
+
catalogFile: catalogFile,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
function parseOperations(raw, shapeCatalogs, file) {
|
|
135
|
+
if (raw === undefined) {
|
|
136
|
+
throw new StreamConfigError(`"operations" is required.`, {
|
|
137
|
+
file,
|
|
138
|
+
pointer: "$.operations",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
142
|
+
throw new StreamConfigError(`"operations" must be an object.`, {
|
|
143
|
+
file,
|
|
144
|
+
pointer: "$.operations",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const out = {};
|
|
148
|
+
for (const [opName, entry] of Object.entries(raw)) {
|
|
149
|
+
if (!opName) {
|
|
150
|
+
throw new StreamConfigError(`"operations" keys must be non-empty strings.`, {
|
|
151
|
+
file,
|
|
152
|
+
pointer: `$.operations`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
out[opName] = parseOperationEntry(opName, entry, shapeCatalogs, file);
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
function parseOperationEntry(opName, raw, shapeCatalogs, file) {
|
|
160
|
+
const pointer = `$.operations.${opName}`;
|
|
161
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
162
|
+
throw new StreamConfigError(`Operation "${opName}" must be an object.`, { file, pointer });
|
|
163
|
+
}
|
|
164
|
+
const e = raw;
|
|
165
|
+
// mode
|
|
166
|
+
const mode = e["mode"];
|
|
167
|
+
if (mode !== undefined && mode !== "stream") {
|
|
168
|
+
throw new StreamConfigError(`Operation "${opName}".mode must be "stream" (or omitted).`, { file, pointer: `${pointer}.mode` });
|
|
169
|
+
}
|
|
170
|
+
// format
|
|
171
|
+
const formatRaw = e["format"] ?? "ndjson";
|
|
172
|
+
if (typeof formatRaw !== "string" || !SUPPORTED_FORMATS.has(formatRaw)) {
|
|
173
|
+
throw new StreamConfigError(`Operation "${opName}".format must be one of: ${[...SUPPORTED_FORMATS].join(", ")}.`, { file, pointer: `${pointer}.format` });
|
|
174
|
+
}
|
|
175
|
+
const format = formatRaw;
|
|
176
|
+
// mediaType (optional; derived from format when absent)
|
|
177
|
+
const mediaTypeRaw = e["mediaType"];
|
|
178
|
+
let mediaType;
|
|
179
|
+
if (mediaTypeRaw === undefined) {
|
|
180
|
+
mediaType = DEFAULT_MEDIA_TYPE_BY_FORMAT[format];
|
|
181
|
+
}
|
|
182
|
+
else if (typeof mediaTypeRaw !== "string" || !mediaTypeRaw.includes("/")) {
|
|
183
|
+
throw new StreamConfigError(`Operation "${opName}".mediaType must be a string of the form "type/subtype".`, { file, pointer: `${pointer}.mediaType` });
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
mediaType = mediaTypeRaw;
|
|
187
|
+
}
|
|
188
|
+
// recordType
|
|
189
|
+
const recordTypeRaw = e["recordType"];
|
|
190
|
+
if (typeof recordTypeRaw !== "string" || !recordTypeRaw) {
|
|
191
|
+
throw new StreamConfigError(`Operation "${opName}".recordType is required and must be a non-empty string.`, { file, pointer: `${pointer}.recordType` });
|
|
192
|
+
}
|
|
193
|
+
const recordTypeName = recordTypeRaw;
|
|
194
|
+
// recordPath
|
|
195
|
+
const recordPathRaw = e["recordPath"];
|
|
196
|
+
if (!Array.isArray(recordPathRaw) || recordPathRaw.length === 0) {
|
|
197
|
+
throw new StreamConfigError(`Operation "${opName}".recordPath must be a non-empty array of element local names.`, { file, pointer: `${pointer}.recordPath` });
|
|
198
|
+
}
|
|
199
|
+
const recordPath = [];
|
|
200
|
+
recordPathRaw.forEach((seg, i) => {
|
|
201
|
+
if (typeof seg !== "string" || !seg) {
|
|
202
|
+
throw new StreamConfigError(`Operation "${opName}".recordPath[${i}] must be a non-empty string.`, { file, pointer: `${pointer}.recordPath[${i}]` });
|
|
203
|
+
}
|
|
204
|
+
recordPath.push(seg);
|
|
205
|
+
});
|
|
206
|
+
// shapeCatalog (optional reference to shapeCatalogs entry)
|
|
207
|
+
const shapeCatalogRaw = e["shapeCatalog"];
|
|
208
|
+
let shapeCatalogName;
|
|
209
|
+
if (shapeCatalogRaw !== undefined) {
|
|
210
|
+
if (typeof shapeCatalogRaw !== "string" || !shapeCatalogRaw) {
|
|
211
|
+
throw new StreamConfigError(`Operation "${opName}".shapeCatalog must be a non-empty string.`, { file, pointer: `${pointer}.shapeCatalog` });
|
|
212
|
+
}
|
|
213
|
+
if (!(shapeCatalogRaw in shapeCatalogs)) {
|
|
214
|
+
throw new StreamConfigError(`Operation "${opName}".shapeCatalog references "${shapeCatalogRaw}" which is not declared under "shapeCatalogs".`, {
|
|
215
|
+
file,
|
|
216
|
+
pointer: `${pointer}.shapeCatalog`,
|
|
217
|
+
suggestion: `Add a "shapeCatalogs.${shapeCatalogRaw}" entry, or remove the reference.`,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
shapeCatalogName = shapeCatalogRaw;
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
mode: "stream",
|
|
224
|
+
format,
|
|
225
|
+
mediaType,
|
|
226
|
+
recordPath,
|
|
227
|
+
recordTypeName,
|
|
228
|
+
shapeCatalogName,
|
|
229
|
+
};
|
|
230
|
+
}
|
package/docs/README.md
CHANGED
|
@@ -27,6 +27,7 @@ Human-maintained reference documents for `@techspokes/typescript-wsdl-client`. T
|
|
|
27
27
|
- [api-reference.md](api-reference.md): programmatic TypeScript API
|
|
28
28
|
- [concepts.md](concepts.md): flattening, `$value`, primitives, determinism
|
|
29
29
|
- [architecture.md](architecture.md): internal pipeline for contributors
|
|
30
|
+
- [decisions/002-streamable-responses.md](decisions/002-streamable-responses.md): opt-in streaming design (client `AsyncIterable`, gateway NDJSON, `x-wsdl-tsc-stream` OpenAPI extension); shipped in 0.17.0
|
|
30
31
|
- [migration.md](migration.md): upgrading between package versions
|
|
31
32
|
|
|
32
33
|
## Conventions
|
package/docs/api-reference.md
CHANGED
|
@@ -37,9 +37,13 @@ function compileWsdlToProject(input: {
|
|
|
37
37
|
wsdl: string;
|
|
38
38
|
outDir: string;
|
|
39
39
|
options?: Partial<CompilerOptions>;
|
|
40
|
+
streamConfigFile?: string;
|
|
41
|
+
streamConfig?: StreamConfig;
|
|
40
42
|
}): Promise<void>;
|
|
41
43
|
```
|
|
42
44
|
|
|
45
|
+
`streamConfigFile` points at a JSON file parsed by `loadStreamConfigFile`. `streamConfig` accepts an already-parsed value; `streamConfigFile` takes precedence when both are set. Buffered generation is unchanged when both are omitted. See [Stream Configuration](configuration.md#stream-configuration) for the file format and [ADR-002](decisions/002-streamable-responses.md) for rationale.
|
|
46
|
+
|
|
43
47
|
### CompilerOptions
|
|
44
48
|
|
|
45
49
|
```typescript
|
|
@@ -219,5 +223,147 @@ interface PipelineOptions {
|
|
|
219
223
|
gateway?: Omit<GenerateGatewayOptions, "openapiFile" | "openapiDocument"> & {
|
|
220
224
|
outDir?: string;
|
|
221
225
|
};
|
|
226
|
+
streamConfigFile?: string;
|
|
227
|
+
streamConfig?: StreamConfig;
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`streamConfigFile` and `streamConfig` are threaded through the compile step and into every downstream emitter via `catalog.json`. Relative paths in `shapeCatalogs` resolve against the stream-config file's directory when `streamConfigFile` is used, otherwise against the WSDL's directory.
|
|
232
|
+
|
|
233
|
+
## Stream Configuration Helpers
|
|
234
|
+
|
|
235
|
+
These exports back the `--stream-config` CLI flag and are also usable directly in programmatic flows.
|
|
236
|
+
|
|
237
|
+
### loadStreamConfigFile
|
|
238
|
+
|
|
239
|
+
Read a stream-config JSON file from disk and return a parsed, validated `StreamConfig`. Throws `StreamConfigError` on missing files, invalid JSON, or schema violations.
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
import { loadStreamConfigFile } from "@techspokes/typescript-wsdl-client";
|
|
243
|
+
|
|
244
|
+
const streamConfig = loadStreamConfigFile("./stream.config.json");
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### parseStreamConfig
|
|
248
|
+
|
|
249
|
+
Parse and validate a stream configuration from an in-memory value. Useful when the config originates from a build tool or a remote source rather than a file.
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
import { parseStreamConfig } from "@techspokes/typescript-wsdl-client";
|
|
253
|
+
|
|
254
|
+
const streamConfig = parseStreamConfig({
|
|
255
|
+
operations: {
|
|
256
|
+
MyStreamOp: {
|
|
257
|
+
recordType: "MyRecordType",
|
|
258
|
+
recordPath: ["MyStreamOpResponse", "Records", "Record"],
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### StreamConfigError
|
|
265
|
+
|
|
266
|
+
Thrown by `loadStreamConfigFile` and `parseStreamConfig` when the configuration is invalid. Use `.toUserMessage()` to render a multi-line, human-readable error suitable for CLI output.
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
import { StreamConfigError } from "@techspokes/typescript-wsdl-client";
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
loadStreamConfigFile("./stream.config.json");
|
|
273
|
+
} catch (err) {
|
|
274
|
+
if (err instanceof StreamConfigError) {
|
|
275
|
+
console.error(err.toUserMessage());
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
throw err;
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### applyShapeCatalogs
|
|
283
|
+
|
|
284
|
+
Resolve companion catalogs against a compiled catalog, copying reachable record-type graphs into place. `runGenerationPipeline` and `compileWsdlToProject` call this automatically when a stream config is present; it is exported for custom pipelines.
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
import {
|
|
288
|
+
applyShapeCatalogs,
|
|
289
|
+
loadStreamConfigFile,
|
|
290
|
+
} from "@techspokes/typescript-wsdl-client";
|
|
291
|
+
import path from "node:path";
|
|
292
|
+
|
|
293
|
+
const streamConfig = loadStreamConfigFile("./stream.config.json");
|
|
294
|
+
await applyShapeCatalogs(compiled, streamConfig, {
|
|
295
|
+
baseDir: path.dirname(path.resolve("./stream.config.json")),
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### ApplyShapeCatalogsOptions
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
interface ApplyShapeCatalogsOptions {
|
|
303
|
+
baseDir?: string;
|
|
222
304
|
}
|
|
223
305
|
```
|
|
306
|
+
|
|
307
|
+
`baseDir` is the directory against which relative `catalogFile` and `wsdlSource` paths are resolved. Defaults to `process.cwd()`.
|
|
308
|
+
|
|
309
|
+
### StreamConfig
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
interface StreamConfig {
|
|
313
|
+
shapeCatalogs: Record<string, ShapeCatalogRef>;
|
|
314
|
+
operations: Record<string, OperationStreamMetadata>;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
interface ShapeCatalogRef {
|
|
318
|
+
wsdlSource?: string;
|
|
319
|
+
catalogFile?: string;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
interface OperationStreamMetadata {
|
|
323
|
+
mode: "stream";
|
|
324
|
+
format: "ndjson" | "json-array";
|
|
325
|
+
mediaType: string;
|
|
326
|
+
recordPath: string[];
|
|
327
|
+
recordTypeName: string;
|
|
328
|
+
shapeCatalogName?: string;
|
|
329
|
+
sourceOutputTypeName?: string;
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Exactly one of `wsdlSource` or `catalogFile` must be set on each `ShapeCatalogRef`. `OperationStreamMetadata` is produced by the parser; `sourceOutputTypeName` is populated by the compiler when it binds the operation to the main WSDL.
|
|
334
|
+
|
|
335
|
+
## End-to-End Example
|
|
336
|
+
|
|
337
|
+
Compile with a stream config, verify the catalog carries the expected metadata, and run the full pipeline:
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
import {
|
|
341
|
+
loadStreamConfigFile,
|
|
342
|
+
runGenerationPipeline,
|
|
343
|
+
} from "@techspokes/typescript-wsdl-client";
|
|
344
|
+
|
|
345
|
+
const streamConfig = loadStreamConfigFile("./stream.config.json");
|
|
346
|
+
|
|
347
|
+
const { compiled, openapiDoc } = await runGenerationPipeline({
|
|
348
|
+
wsdl: "./wsdl/Service.wsdl",
|
|
349
|
+
catalogOut: "./build/service-catalog.json",
|
|
350
|
+
clientOutDir: "./src/services/service",
|
|
351
|
+
compiler: { imports: "js" },
|
|
352
|
+
openapi: {
|
|
353
|
+
outFile: "./docs/service-api.json",
|
|
354
|
+
format: "json",
|
|
355
|
+
servers: ["https://api.example.com/v1"],
|
|
356
|
+
},
|
|
357
|
+
gateway: {
|
|
358
|
+
outDir: "./src/gateway/service",
|
|
359
|
+
versionSlug: "v1",
|
|
360
|
+
serviceSlug: "service",
|
|
361
|
+
},
|
|
362
|
+
streamConfig,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const streamOp = compiled.operations.find((op) => op.stream);
|
|
366
|
+
console.log(streamOp?.stream?.mediaType); // "application/x-ndjson"
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
The generated client exports a `StreamOperationResponse<RecordType>` type; each stream-configured operation returns `Promise<StreamOperationResponse<RecordType>>` with `records: AsyncIterable<RecordType>`.
|
package/docs/architecture.md
CHANGED
|
@@ -7,9 +7,9 @@ See [CONTRIBUTING](../CONTRIBUTING.md) for development setup and [README](../REA
|
|
|
7
7
|
## Pipeline Overview
|
|
8
8
|
|
|
9
9
|
```text
|
|
10
|
-
WSDL Source
|
|
11
|
-
|
|
|
12
|
-
v
|
|
10
|
+
WSDL Source Stream Config (optional)
|
|
11
|
+
| |
|
|
12
|
+
v v
|
|
13
13
|
Loader (fetch.ts, wsdlLoader.ts)
|
|
14
14
|
Fetches WSDL from URL or file
|
|
15
15
|
Returns parsed XML document
|
|
@@ -17,9 +17,16 @@ Loader (fetch.ts, wsdlLoader.ts)
|
|
|
17
17
|
v
|
|
18
18
|
Compiler (schemaCompiler.ts)
|
|
19
19
|
Walks XSD types, resolves references
|
|
20
|
+
Retains xs:any wildcard particles
|
|
20
21
|
Produces CompiledCatalog
|
|
21
22
|
|
|
|
22
23
|
v
|
|
24
|
+
Shape Resolver (shapeResolver.ts, optional)
|
|
25
|
+
Loads companion catalogs
|
|
26
|
+
Copies reachable record-type graphs
|
|
27
|
+
Fails on structural name collisions
|
|
28
|
+
|
|
|
29
|
+
v
|
|
23
30
|
Catalog Emitter (generateCatalog.ts)
|
|
24
31
|
Serializes to catalog.json
|
|
25
32
|
|
|
|
@@ -38,7 +45,7 @@ fetch.ts handles HTTP/HTTPS/file fetching. wsdlLoader.ts handles XML parsing and
|
|
|
38
45
|
|
|
39
46
|
### compiler/
|
|
40
47
|
|
|
41
|
-
schemaCompiler.ts is the core compiler. It walks XSD complex/simple types, resolves inheritance, handles choices, and builds the type graph. generateCatalog.ts serializes compiled types to catalog.json.
|
|
48
|
+
schemaCompiler.ts is the core compiler. It walks XSD complex/simple types, resolves inheritance, handles choices, retains xs:any wildcard particles as catalog metadata, and builds the type graph. generateCatalog.ts serializes compiled types to catalog.json. shapeResolver.ts resolves companion catalogs: it loads each referenced `shapeCatalog` once, copies the reachable record-type graph into the current compilation, and enforces structural-equality collision checks. It mutates the compiled catalog in place and is invoked after schemaCompiler.ts whenever a stream config is present.
|
|
42
49
|
|
|
43
50
|
### client/
|
|
44
51
|
|
|
@@ -62,7 +69,11 @@ pipeline.ts orchestrates the full pipeline from compile through app generation.
|
|
|
62
69
|
|
|
63
70
|
### util/
|
|
64
71
|
|
|
65
|
-
tools.ts provides string helpers (pascal, kebab, QName resolution). cli.ts provides console output helpers and error handling. builder.ts provides shared Yargs option builders.
|
|
72
|
+
tools.ts provides string helpers (pascal, kebab, QName resolution). cli.ts provides console output helpers and error handling. builder.ts provides shared Yargs option builders. streamConfig.ts parses and validates `--stream-config` input into a normalized `StreamConfig` shape; it is parser-only and never touches the filesystem beyond reading the config file itself. runtimeSource.ts reads template strings for the client stream transport.
|
|
73
|
+
|
|
74
|
+
### runtime/
|
|
75
|
+
|
|
76
|
+
Template sources embedded into generated clients and gateways. streamXml.ts implements a SAX-driven `parseRecords(stream, spec)` that tracks the configured `recordPath` positionally (duplicate local names allowed) and yields records as their closing tags arrive. ndjson.ts wraps an async iterable of records in a `Readable` that emits NDJSON lines with honored backpressure. clientStreamMethods.tpl.txt and operationsStreamHelper.tpl.txt are text templates emitted into the generated `client.ts` and `operations.ts` when stream operations are present; they encode the `callStream()` transport and the `StreamOperationResponse<T>` type.
|
|
66
77
|
|
|
67
78
|
### xsd/
|
|
68
79
|
|
|
@@ -115,3 +126,14 @@ Data flow through the pipeline:
|
|
|
115
126
|
1. Add mapping in src/xsd/primitives.ts
|
|
116
127
|
2. Handle in src/compiler/schemaCompiler.ts if needed
|
|
117
128
|
3. Update src/openapi/generateSchemas.ts for JSON Schema output
|
|
129
|
+
|
|
130
|
+
### Adding a Stream-Capable Operation Output
|
|
131
|
+
|
|
132
|
+
1. Add a `stream-config` JSON entry for the operation with `recordType` and `recordPath`
|
|
133
|
+
2. Declare a `shapeCatalog` entry when the record type lives in a companion WSDL
|
|
134
|
+
3. Run the compile command and verify `catalog.operations[].stream` carries normalized metadata
|
|
135
|
+
4. The client, OpenAPI, and gateway emitters consume that metadata automatically; no per-emitter code changes are required
|
|
136
|
+
|
|
137
|
+
## Client Stream Transport
|
|
138
|
+
|
|
139
|
+
Phase-0 research (see ADR-002) established that `node-soap` buffers the full response before invoking its operation callback. Stream operations cannot use that transport, so the generated client emits a parallel `callStream()` method that POSTs a hand-built SOAP envelope via global `fetch`, captures HTTP headers before the first record, and pipes the response body through `parseRecords`. Buffered operations continue to use `node-soap` unchanged. The two transports coexist on the same client class.
|
package/docs/cli-reference.md
CHANGED
|
@@ -129,6 +129,36 @@ The catalog is auto-placed alongside the first available output directory: `{cli
|
|
|
129
129
|
| `--test-dir` | (none) | Generate Vitest test suite in this directory (requires client, gateway) |
|
|
130
130
|
| `--force-test` | `false` | Overwrite existing test files when using --test-dir |
|
|
131
131
|
|
|
132
|
+
### Stream Configuration Flags
|
|
133
|
+
|
|
134
|
+
| Flag | Default | Description |
|
|
135
|
+
|------|---------|-------------|
|
|
136
|
+
| `--stream-config` | (none) | Path to a JSON stream-configuration file (ADR-002). Marks selected operations as streaming: client emits `AsyncIterable<RecordType>`, gateway serves NDJSON, OpenAPI advertises the record schema via `x-wsdl-tsc-stream`. Buffered output is unchanged when the flag is omitted. |
|
|
137
|
+
|
|
138
|
+
`--stream-config` is accepted on the `compile`, `client`, and `pipeline` commands. It is not accepted on `openapi`, `gateway`, or `app` because those consume a pre-compiled `catalog.json` that already carries the normalized stream metadata.
|
|
139
|
+
|
|
140
|
+
Stream-config file shape (see `docs/decisions/002-streamable-responses.md`):
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"shapeCatalogs": {
|
|
145
|
+
"main": { "wsdlSource": "https://api.example.com/Main.svc?singleWsdl" }
|
|
146
|
+
},
|
|
147
|
+
"operations": {
|
|
148
|
+
"UnitDescriptiveInfoStream": {
|
|
149
|
+
"recordType": "UnitDescriptiveContentType",
|
|
150
|
+
"recordPath": [
|
|
151
|
+
"UnitDescriptiveInfoStream",
|
|
152
|
+
"EVRN_UnitDescriptiveInfoRS",
|
|
153
|
+
"UnitDescriptiveContents",
|
|
154
|
+
"UnitDescriptiveContent"
|
|
155
|
+
],
|
|
156
|
+
"shapeCatalog": "main"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
132
162
|
### Pipeline Workflow
|
|
133
163
|
|
|
134
164
|
Steps execute in order:
|
package/docs/concepts.md
CHANGED
|
@@ -230,6 +230,10 @@ details:
|
|
|
230
230
|
}
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
+
### Streaming Bypass
|
|
234
|
+
|
|
235
|
+
Operations opted into streaming with `--stream-config` bypass the success envelope on the `200` response path. The OpenAPI response content is declared as the configured stream media type (default `application/x-ndjson`) and the gateway writes raw NDJSON lines straight to the response body. Error responses (400, 502, and the rest) still use the normal envelope so clients always see structured failures before the first record. See [ADR-002](decisions/002-streamable-responses.md) for the full rationale.
|
|
236
|
+
|
|
233
237
|
### Envelope Naming
|
|
234
238
|
|
|
235
239
|
The base envelope is named `${serviceName}ResponseEnvelope`. Override with
|
|
@@ -337,3 +341,50 @@ schemas, and detects circular dependencies.
|
|
|
337
341
|
|
|
338
342
|
Disable with `--openapi-validate false` or `validate: false` in the
|
|
339
343
|
programmatic API.
|
|
344
|
+
|
|
345
|
+
## Streaming vs Buffered Responses
|
|
346
|
+
|
|
347
|
+
The generator produces two response execution models. Buffered is the default and only path for operations not listed in a stream config. Streaming is opt-in and operation-scoped; it changes the emitted client method signature, the OpenAPI response description, and the Fastify route shape for that operation only.
|
|
348
|
+
|
|
349
|
+
### Execution Model Contrast
|
|
350
|
+
|
|
351
|
+
Buffered operations call `node-soap`, wait for the full response to materialize, and return `{ response, headers, responseRaw, requestRaw }`. Streaming operations bypass `node-soap`, POST a hand-built SOAP envelope via `fetch`, and return `StreamOperationResponse<RecordType>` with `records: AsyncIterable<RecordType>`. The SAX parser in `runtime/streamXml.ts` walks the configured `recordPath` and yields each record as its closing tag arrives.
|
|
352
|
+
|
|
353
|
+
The catalog is the source of truth for stream metadata. Each opted-in operation carries an `OperationStreamMetadata` entry, and downstream emitters (client, OpenAPI, gateway, tests) all read from the catalog. OpenAPI carries a derived view via the `x-wsdl-tsc-stream` extension.
|
|
354
|
+
|
|
355
|
+
### Terminal-Error Policy
|
|
356
|
+
|
|
357
|
+
Errors before the first record use the normal gateway error envelope because the response headers and status have not been committed yet. Errors mid-stream truncate the chunked response without a terminating zero-chunk; consumers detect this as an incomplete HTTP response and must treat it as a failure. NDJSON has no native error frame, and emitting a fake one would conflict with the item schema. This behavior is documented for operators in the [Production Guide](production.md#terminal-error-policy).
|
|
358
|
+
|
|
359
|
+
## Companion Catalogs and Shape Resolution
|
|
360
|
+
|
|
361
|
+
Some vendor WSDLs split their types across multiple services. The stream wrapper operation lives in one WSDL while the concrete record type lives in a companion WSDL. The stream config's `shapeCatalogs` section names additional WSDL or catalog inputs used only to resolve record shapes.
|
|
362
|
+
|
|
363
|
+
### How Resolution Works
|
|
364
|
+
|
|
365
|
+
When an operation names a `shapeCatalog`, the compiler loads the companion catalog once, copies the reachable record-type graph into the current compilation, and fails loudly on structural name collisions. Structurally identical types dedupe silently, so two catalogs that share a common base type do not conflict.
|
|
366
|
+
|
|
367
|
+
```json
|
|
368
|
+
{
|
|
369
|
+
"shapeCatalogs": {
|
|
370
|
+
"main": { "wsdlSource": "https://api.example.com/Main.svc?singleWsdl" }
|
|
371
|
+
},
|
|
372
|
+
"operations": {
|
|
373
|
+
"StreamOp": {
|
|
374
|
+
"recordType": "ConcreteRecordType",
|
|
375
|
+
"recordPath": ["StreamOpResponse", "Records", "Record"],
|
|
376
|
+
"shapeCatalog": "main"
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Collision Handling
|
|
383
|
+
|
|
384
|
+
A structural collision means two types share a name but differ in fields. The build fails with a diagnostic naming both source catalogs. Rename in the companion source or point `recordType` at a distinct subtree. Silent renames are intentionally disallowed because they would produce ambiguous public APIs.
|
|
385
|
+
|
|
386
|
+
## xs:any Wildcard Retention
|
|
387
|
+
|
|
388
|
+
XSD wildcards (`<xs:any>`) were silently dropped by earlier compiler versions. Since 0.17.0 the compiler retains them on the compiled type alongside any concrete children. This enables two downstream behaviors: honest stream-candidate detection (a wrapper that contains a wildcard is a likely streaming target) and accurate companion-catalog resolution (the compiler knows which elements are open for record types to slot into).
|
|
389
|
+
|
|
390
|
+
The retained wildcard is metadata only. It does not emit TypeScript `any` or loosen the generated types; concrete fields remain strictly typed.
|
package/docs/configuration.md
CHANGED
|
@@ -65,6 +65,46 @@ Per-operation overrides for method, summary, description, and deprecation.
|
|
|
65
65
|
}
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
## Stream Configuration
|
|
69
|
+
|
|
70
|
+
The `--stream-config <file>` flag (available on `compile`, `client`, `pipeline`)
|
|
71
|
+
opts selected WSDL operations into streaming. Buffered output is unchanged for
|
|
72
|
+
operations not listed in the file.
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"shapeCatalogs": {
|
|
77
|
+
"main": { "wsdlSource": "https://api.example.com/Main.svc?singleWsdl" }
|
|
78
|
+
},
|
|
79
|
+
"operations": {
|
|
80
|
+
"UnitDescriptiveInfoStream": {
|
|
81
|
+
"format": "ndjson",
|
|
82
|
+
"mediaType": "application/x-ndjson",
|
|
83
|
+
"recordType": "UnitDescriptiveContentType",
|
|
84
|
+
"recordPath": [
|
|
85
|
+
"UnitDescriptiveInfoStream",
|
|
86
|
+
"EVRN_UnitDescriptiveInfoRS",
|
|
87
|
+
"UnitDescriptiveContents",
|
|
88
|
+
"UnitDescriptiveContent"
|
|
89
|
+
],
|
|
90
|
+
"shapeCatalog": "main"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- `recordType` and `recordPath` are required; `format` defaults to `ndjson` and
|
|
97
|
+
`mediaType` to `application/x-ndjson`.
|
|
98
|
+
- `shapeCatalog` references a `shapeCatalogs` entry and is only needed when
|
|
99
|
+
the record type lives in a different WSDL than the one driving generation.
|
|
100
|
+
- Shape catalogs accept either `wsdlSource` (fetched and compiled on the fly)
|
|
101
|
+
or `catalogFile` (path to a pre-compiled `catalog.json`).
|
|
102
|
+
- Structural name collisions across catalogs fail the build; structurally
|
|
103
|
+
identical types dedupe silently.
|
|
104
|
+
|
|
105
|
+
See [ADR-002](decisions/002-streamable-responses.md) for rationale and the
|
|
106
|
+
terminal-error policy.
|
|
107
|
+
|
|
68
108
|
## Example Files
|
|
69
109
|
|
|
70
110
|
Example configuration files are available in the `examples/openapi/` directory:
|