@usearazzo/runner 1.0.1-alpha.11 → 1.0.1-alpha.13
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/CHANGELOG.md +18 -0
- package/README.md +1 -3
- package/dist/arazzo-runner.browser.js +2053 -146
- package/dist/arazzo-runner.browser.min.js +1 -1
- package/package.json +5 -4
- package/src/client/OpenAPIOperationResponse.cjs +37 -2
- package/src/client/OpenAPIOperationResponse.mjs +37 -2
- package/src/client/OpenAPIOperationResponseNormalizer.cjs +47 -3
- package/src/client/OpenAPIOperationResponseNormalizer.mjs +48 -3
- package/src/executor/OpenAPIOperationExecutor.cjs +144 -1
- package/src/executor/OpenAPIOperationExecutor.mjs +145 -1
- package/types/runner.d.ts +30 -3
|
@@ -2,6 +2,7 @@ import { toValue } from '@speclynx/apidom-core';
|
|
|
2
2
|
import { isArrayElement, isObjectElement } from '@speclynx/apidom-datamodel';
|
|
3
3
|
import { url as urlUtils } from '@speclynx/apidom-reference/configuration/empty';
|
|
4
4
|
import { isSwaggerElement, isParameterElement } from '@speclynx/apidom-ns-openapi-2';
|
|
5
|
+
import { serializeCookie, cookieNameLenientValidator, cookieValueStrictPercentEncoder } from '@swaggerexpert/cookie';
|
|
5
6
|
import { identify, matches, selectBestMatch } from '@swaggerexpert/media-types';
|
|
6
7
|
import { isPlainObject } from 'ramda-adjunct';
|
|
7
8
|
|
|
@@ -22,6 +23,19 @@ import ClientError from "../errors/ClientError.mjs";
|
|
|
22
23
|
* May be synchronous or asynchronous.
|
|
23
24
|
* @public
|
|
24
25
|
*/
|
|
26
|
+
/**
|
|
27
|
+
* The plain-JSON shape of the assembled document, as far as declaring a
|
|
28
|
+
* parameter on its one operation needs it.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* A caller-supplied parameter the operation does not declare, in a location
|
|
32
|
+
* it can still be delivered to.
|
|
33
|
+
*/
|
|
34
|
+
const UNDECLARED_LOCATIONS = new Set(['header', 'query', 'cookie', 'path']);
|
|
35
|
+
|
|
36
|
+
// the header parameters OpenAPI 3 says SHALL be ignored, which buildRequest drops
|
|
37
|
+
const IGNORED_HEADERS = new Set(['accept', 'authorization', 'content-type']);
|
|
38
|
+
|
|
25
39
|
/**
|
|
26
40
|
* Options for executing an OpenAPI operation.
|
|
27
41
|
*
|
|
@@ -39,6 +53,7 @@ import ClientError from "../errors/ClientError.mjs";
|
|
|
39
53
|
* {@link RequestInterceptor}.
|
|
40
54
|
* @public
|
|
41
55
|
*/
|
|
56
|
+
|
|
42
57
|
/**
|
|
43
58
|
* The execute options forwarded to {@link OpenAPIOperationExecutor.execute}.
|
|
44
59
|
*
|
|
@@ -61,10 +76,12 @@ import ClientError from "../errors/ClientError.mjs";
|
|
|
61
76
|
* `responseNormalizer` — reach `buildRequest` but nothing acts on them.
|
|
62
77
|
* @public
|
|
63
78
|
*/
|
|
79
|
+
|
|
64
80
|
/**
|
|
65
81
|
* Options for the OpenAPIOperationExecutor.
|
|
66
82
|
* @public
|
|
67
83
|
*/
|
|
84
|
+
|
|
68
85
|
/**
|
|
69
86
|
* Executes a single OpenAPI operation named by a canonical
|
|
70
87
|
* {@link OpenAPIOperationLocator}.
|
|
@@ -137,9 +154,11 @@ class OpenAPIOperationExecutor {
|
|
|
137
154
|
const withServer = this.#applyServerOptions(rest, normalizedOperation, locator.document);
|
|
138
155
|
const withBody = this.#adaptRequestBody(withServer, normalizedOperation, locator.document, locator.jsonPointer);
|
|
139
156
|
const buildOptions = this.#adaptRequestContentType(withBody, normalizedOperation, locator.document, locator.jsonPointer);
|
|
157
|
+
const undeclared = this.#undeclaredParameters(buildOptions.parameters, normalizedOperation);
|
|
158
|
+
const spec = assembled.toJSON();
|
|
159
|
+
this.#declareQueryAndPathParameters(spec, undeclared, toValue(operation.meta.get('path')), toValue(operation.meta.get('http-method')), locator);
|
|
140
160
|
let request;
|
|
141
161
|
try {
|
|
142
|
-
const spec = assembled.toJSON();
|
|
143
162
|
request = buildRequest({
|
|
144
163
|
...buildOptions,
|
|
145
164
|
spec,
|
|
@@ -159,6 +178,7 @@ class OpenAPIOperationExecutor {
|
|
|
159
178
|
if (typeof authoredContentType === 'string' && buildOptions.requestContentType !== authoredContentType) {
|
|
160
179
|
request.headers['Content-Type'] = authoredContentType;
|
|
161
180
|
}
|
|
181
|
+
this.#deliverHeadersAndCookies(request, undeclared);
|
|
162
182
|
request = await this.#finalizeRequest(request, requestInterceptor);
|
|
163
183
|
|
|
164
184
|
// capture the request as sent, so `$url` / `$method` / `$request.*` can be
|
|
@@ -184,6 +204,130 @@ class OpenAPIOperationExecutor {
|
|
|
184
204
|
return this.#responseNormalizer.normalize(rawResponse, requestInfo);
|
|
185
205
|
}
|
|
186
206
|
|
|
207
|
+
/**
|
|
208
|
+
* The `{in}.{name}`-keyed parameters (see {@link ParameterDelivery}) in a
|
|
209
|
+
* header, query, cookie or path location that the operation does not
|
|
210
|
+
* declare. `buildRequest` delivers only what the operation declares, yet a
|
|
211
|
+
* step's parameters are what its author asked to send — an `Authorization`
|
|
212
|
+
* header carrying a token from an earlier step most of all, which OpenAPI 3
|
|
213
|
+
* cannot even declare. A declared `Accept`, `Authorization` or
|
|
214
|
+
* `Content-Type` header parameter counts as undeclared too: `buildRequest`
|
|
215
|
+
* drops it for OpenAPI 3, as that specification requires, and for 2.0
|
|
216
|
+
* delivering it here sends the same value. Bare keys are left to
|
|
217
|
+
* `buildRequest`: they name no location to deliver to.
|
|
218
|
+
*/
|
|
219
|
+
#undeclaredParameters(parameters, operation) {
|
|
220
|
+
if (!isPlainObject(parameters)) return [];
|
|
221
|
+
const declared = new Set((isArrayElement(operation.parameters) ? [...operation.parameters] : []).filter(isObjectElement).map(parameter => `${toValue(parameter.get('in'))}.${toValue(parameter.get('name'))}`));
|
|
222
|
+
return Object.entries(parameters).flatMap(([key, value]) => {
|
|
223
|
+
const separator = key.indexOf('.');
|
|
224
|
+
const location = key.slice(0, separator);
|
|
225
|
+
const name = key.slice(separator + 1);
|
|
226
|
+
// an unresolved expression or a null delivers nothing, as buildRequest
|
|
227
|
+
// does for a declared parameter
|
|
228
|
+
if (separator === -1 || !UNDECLARED_LOCATIONS.has(location) || value === undefined || value === null) {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
const ignored = location === 'header' && IGNORED_HEADERS.has(name.toLowerCase());
|
|
232
|
+
return declared.has(key) && !ignored ? [] : [{
|
|
233
|
+
location,
|
|
234
|
+
name,
|
|
235
|
+
value
|
|
236
|
+
}];
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Declares the undeclared query and path parameters on the assembled
|
|
242
|
+
* operation, so `buildRequest` serializes and encodes them as it does
|
|
243
|
+
* declared ones. A path parameter is declared only when the path has its
|
|
244
|
+
* `{name}` template expression; otherwise there is nowhere to put it, and
|
|
245
|
+
* that authoring error throws {@link ClientError} rather than sending a
|
|
246
|
+
* request without it.
|
|
247
|
+
*/
|
|
248
|
+
#declareQueryAndPathParameters(spec, undeclared, path, method, locator) {
|
|
249
|
+
const operation = spec.paths[path][method];
|
|
250
|
+
const isSwagger = isSwaggerElement(locator.document.parseResult.api);
|
|
251
|
+
for (const {
|
|
252
|
+
location,
|
|
253
|
+
name
|
|
254
|
+
} of undeclared) {
|
|
255
|
+
if (location !== 'query' && location !== 'path') continue;
|
|
256
|
+
if (location === 'path' && !path.includes(`{${name}}`)) {
|
|
257
|
+
throw new ClientError(`Path parameter "${name}" is not declared by the operation and its path "${path}" ` + `has no "{${name}}" to substitute it into`, {
|
|
258
|
+
operationPath: locator.jsonPointer
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const required = location === 'path';
|
|
262
|
+
(operation.parameters ??= []).push(isSwagger ? {
|
|
263
|
+
name,
|
|
264
|
+
in: location,
|
|
265
|
+
required,
|
|
266
|
+
type: 'string'
|
|
267
|
+
} : {
|
|
268
|
+
name,
|
|
269
|
+
in: location,
|
|
270
|
+
required,
|
|
271
|
+
schema: {}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Sets the undeclared header parameters on the built request and appends the
|
|
278
|
+
* undeclared cookie parameters to its `Cookie` header. A header replaces any
|
|
279
|
+
* value the build gave it under any spelling — the default `Accept`, say —
|
|
280
|
+
* since the step naming it is the more specific intent. OpenAPI 2.0 has no
|
|
281
|
+
* cookie location, so its cookies always go this way.
|
|
282
|
+
*/
|
|
283
|
+
#deliverHeadersAndCookies(request, undeclared) {
|
|
284
|
+
const present = name => Object.keys(request.headers).find(header => header.toLowerCase() === name.toLowerCase());
|
|
285
|
+
for (const {
|
|
286
|
+
location,
|
|
287
|
+
name,
|
|
288
|
+
value
|
|
289
|
+
} of undeclared) {
|
|
290
|
+
if (location !== 'header') continue;
|
|
291
|
+
const built = present(name);
|
|
292
|
+
if (built !== undefined) delete request.headers[built];
|
|
293
|
+
request.headers[name] = this.#stringify(value);
|
|
294
|
+
}
|
|
295
|
+
const cookies = undeclared.filter(({
|
|
296
|
+
location
|
|
297
|
+
}) => location === 'cookie').map(({
|
|
298
|
+
name,
|
|
299
|
+
value
|
|
300
|
+
}) => [name, this.#stringify(value)]);
|
|
301
|
+
if (cookies.length === 0) return;
|
|
302
|
+
// serialized as buildRequest serializes a declared cookie, so a `;` in the
|
|
303
|
+
// value is percent-encoded rather than splitting it into further cookies
|
|
304
|
+
const serialized = serializeCookie(cookies, {
|
|
305
|
+
encoders: {
|
|
306
|
+
value: cookieValueStrictPercentEncoder
|
|
307
|
+
},
|
|
308
|
+
validators: {
|
|
309
|
+
name: cookieNameLenientValidator
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
const cookieHeader = present('Cookie') ?? 'Cookie';
|
|
313
|
+
const existing = request.headers[cookieHeader];
|
|
314
|
+
request.headers[cookieHeader] = existing ? `${existing}; ${serialized}` : serialized;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* A header or cookie value with no schema to serialize it by, the way
|
|
319
|
+
* OpenAPI's default for those locations (`simple` and `form`, not exploded)
|
|
320
|
+
* serializes a declared one: an array as `a,b`, an object as `k,v,k2,v2`,
|
|
321
|
+
* anything else via `String()`.
|
|
322
|
+
*/
|
|
323
|
+
#stringify(value) {
|
|
324
|
+
if (Array.isArray(value)) return value.map(String).join(',');
|
|
325
|
+
if (typeof value === 'object' && value !== null) {
|
|
326
|
+
return Object.entries(value).flat().map(String).join(',');
|
|
327
|
+
}
|
|
328
|
+
return String(value);
|
|
329
|
+
}
|
|
330
|
+
|
|
187
331
|
/**
|
|
188
332
|
* Derives where the request is sent, returning the options to build with.
|
|
189
333
|
*
|
package/types/runner.d.ts
CHANGED
|
@@ -1261,15 +1261,23 @@ export declare interface OpenAPIOperationRequestInfo {
|
|
|
1261
1261
|
|
|
1262
1262
|
/**
|
|
1263
1263
|
* Response from an OpenAPI operation execution.
|
|
1264
|
+
*
|
|
1265
|
+
* `body` is the payload parsed by its content type, what `$response.body`
|
|
1266
|
+
* reads. The body as received is read through `text()`, `blob()` and
|
|
1267
|
+
* `bytes()`, named after the WHATWG `Response` methods but synchronous, and
|
|
1268
|
+
* empty when the response carried no body (e.g. `204`, `304`). `bytes()` and
|
|
1269
|
+
* `blob()` always return the bytes received. A body that is valid UTF-8 is held
|
|
1270
|
+
* as its decoded string, which a plain-text or XML `body` shares rather than
|
|
1271
|
+
* duplicates, and is encoded back on read; any other body is held as bytes.
|
|
1264
1272
|
* @public
|
|
1265
1273
|
*/
|
|
1266
1274
|
export declare class OpenAPIOperationResponse {
|
|
1275
|
+
#private;
|
|
1267
1276
|
readonly ok: boolean;
|
|
1268
1277
|
readonly url: string;
|
|
1269
1278
|
readonly status: number;
|
|
1270
1279
|
readonly statusText: string;
|
|
1271
1280
|
readonly headers: Record<string, string>;
|
|
1272
|
-
readonly text: string;
|
|
1273
1281
|
readonly body: unknown;
|
|
1274
1282
|
readonly request: OpenAPIOperationRequestInfo | undefined;
|
|
1275
1283
|
constructor(raw: {
|
|
@@ -1278,10 +1286,24 @@ export declare class OpenAPIOperationResponse {
|
|
|
1278
1286
|
status: number;
|
|
1279
1287
|
statusText: string;
|
|
1280
1288
|
headers: Record<string, string>;
|
|
1281
|
-
text: string;
|
|
1282
1289
|
body: unknown;
|
|
1290
|
+
content: string | Uint8Array;
|
|
1283
1291
|
request?: OpenAPIOperationRequestInfo;
|
|
1284
1292
|
});
|
|
1293
|
+
/**
|
|
1294
|
+
* The body as text, like `Response.text()`; `''` when there is none.
|
|
1295
|
+
*/
|
|
1296
|
+
text(): string;
|
|
1297
|
+
/**
|
|
1298
|
+
* The body as a `Blob` typed by the response's `Content-Type`, like
|
|
1299
|
+
* `Response.blob()`.
|
|
1300
|
+
*/
|
|
1301
|
+
blob(): Blob;
|
|
1302
|
+
/**
|
|
1303
|
+
* The body as bytes, like `Response.bytes()`; a fresh copy on every call, so
|
|
1304
|
+
* the response stays immutable.
|
|
1305
|
+
*/
|
|
1306
|
+
bytes(): Uint8Array;
|
|
1285
1307
|
}
|
|
1286
1308
|
|
|
1287
1309
|
/**
|
|
@@ -1295,10 +1317,15 @@ export declare class OpenAPIOperationResponse {
|
|
|
1295
1317
|
* Headers are taken from the `Response` directly rather than from the
|
|
1296
1318
|
* deserializer, which splits every value containing `", "` into an array:
|
|
1297
1319
|
* values stay strings as sent, with repeated fields (`Set-Cookie` included)
|
|
1298
|
-
* combined by `Headers.get`.
|
|
1320
|
+
* combined by `Headers.get`. The body is read once, as bytes, and handed to the
|
|
1321
|
+
* deserializer from memory. A body that decodes as UTF-8 without loss is kept
|
|
1322
|
+
* as that one string, the same one the deserializer parses, so a plain-text or
|
|
1323
|
+
* XML `body` shares it; any other body (binary, another charset, a leading
|
|
1324
|
+
* byte-order mark) is kept as the bytes, so they are never re-encoded lossily.
|
|
1299
1325
|
* @public
|
|
1300
1326
|
*/
|
|
1301
1327
|
export declare class OpenAPIOperationResponseNormalizer {
|
|
1328
|
+
#private;
|
|
1302
1329
|
/**
|
|
1303
1330
|
* Deserializes the response body by content type and maps the result into the
|
|
1304
1331
|
* runner's response model. `request` is the request the operation was
|