poe-code 3.0.289 → 3.0.290
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/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/packages/toolcraft-openapi/dist/mock/fetch.js +187 -19
package/package.json
CHANGED
|
@@ -34,6 +34,10 @@ export async function mockFetch(options) {
|
|
|
34
34
|
body: parsedBody,
|
|
35
35
|
at: new Date()
|
|
36
36
|
});
|
|
37
|
+
const parameterErrors = validateRequestParameters(operation, requestUrl, headers, document);
|
|
38
|
+
if (parameterErrors.length > 0) {
|
|
39
|
+
return jsonResponse(422, { errors: parameterErrors });
|
|
40
|
+
}
|
|
37
41
|
if (operation.requestBodySchema !== undefined && parsedBody !== undefined) {
|
|
38
42
|
const errors = validateAgainstSchema(parsedBody, operation.requestBodySchema, document, "$");
|
|
39
43
|
if (errors.length > 0) {
|
|
@@ -46,7 +50,7 @@ export async function mockFetch(options) {
|
|
|
46
50
|
const fixture = await fixtureLoader(operation.operationId);
|
|
47
51
|
if (fixture !== undefined) {
|
|
48
52
|
const status = fixture.status ?? operation.defaultStatus;
|
|
49
|
-
const responseSchema = operation.responseSchemas
|
|
53
|
+
const responseSchema = findResponseSchema(operation.responseSchemas, status);
|
|
50
54
|
if (responseSchema !== undefined && fixture.body !== undefined) {
|
|
51
55
|
const errors = validateAgainstSchema(fixture.body, responseSchema, document, "$response");
|
|
52
56
|
if (errors.length > 0) {
|
|
@@ -110,13 +114,16 @@ function compileOperations(document) {
|
|
|
110
114
|
const operationId = resolvedOperation.operationId ?? `${method.toUpperCase()} ${pathTemplate}`;
|
|
111
115
|
const { defaultStatus, defaultExample, responseSchemas } = pickResponseMetadata(resolvedOperation, document);
|
|
112
116
|
const { schema: requestBodySchema, required: requestBodyRequired } = pickRequestBody(resolvedOperation, document);
|
|
117
|
+
const pathParameterNames = collectPathParameterNames(pathTemplate);
|
|
113
118
|
compiled.push({
|
|
114
119
|
method: method.toUpperCase(),
|
|
115
120
|
pathTemplate,
|
|
116
121
|
pathRegex: pathTemplateToRegex(pathTemplate),
|
|
122
|
+
pathParameterNames,
|
|
117
123
|
pathSpecificity: countPathPlaceholders(pathTemplate),
|
|
118
124
|
operationId,
|
|
119
125
|
operation: resolvedOperation,
|
|
126
|
+
parameters: collectCompiledParameters(pathItem.parameters, resolvedOperation.parameters, document),
|
|
120
127
|
defaultStatus,
|
|
121
128
|
defaultExample,
|
|
122
129
|
requestBodySchema,
|
|
@@ -131,14 +138,31 @@ function compileOperations(document) {
|
|
|
131
138
|
return compiled;
|
|
132
139
|
}
|
|
133
140
|
function countPathPlaceholders(template) {
|
|
134
|
-
return (template
|
|
141
|
+
return collectPathParameterNames(template).length;
|
|
135
142
|
}
|
|
136
143
|
function pathTemplateToRegex(template) {
|
|
137
144
|
// Escape regex metacharacters except for "{...}" placeholders, which become non-slash captures.
|
|
138
145
|
const pattern = template.replace(/[.*+?^${}()|[\]\\]/g, (match) => match === "{" || match === "}" ? match : `\\${match}`);
|
|
139
|
-
const withParams = pattern.replace(/\{[^}]+\}/g, "[^/]+");
|
|
146
|
+
const withParams = pattern.replace(/\{[^}]+\}/g, "([^/]+)");
|
|
140
147
|
return new RegExp(`^${withParams}$`);
|
|
141
148
|
}
|
|
149
|
+
function collectPathParameterNames(template) {
|
|
150
|
+
const names = [];
|
|
151
|
+
let index = 0;
|
|
152
|
+
while (index < template.length) {
|
|
153
|
+
const start = template.indexOf("{", index);
|
|
154
|
+
if (start === -1) {
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
const end = template.indexOf("}", start + 1);
|
|
158
|
+
if (end === -1) {
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
names.push(template.slice(start + 1, end));
|
|
162
|
+
index = end + 1;
|
|
163
|
+
}
|
|
164
|
+
return names;
|
|
165
|
+
}
|
|
142
166
|
function resolveOperation(operation, document) {
|
|
143
167
|
if (isReference(operation)) {
|
|
144
168
|
return resolveReference(operation, document);
|
|
@@ -147,10 +171,11 @@ function resolveOperation(operation, document) {
|
|
|
147
171
|
}
|
|
148
172
|
function pickResponseMetadata(operation, document) {
|
|
149
173
|
const responses = operation.responses ?? {};
|
|
150
|
-
const responseSchemas =
|
|
174
|
+
const responseSchemas = [];
|
|
175
|
+
const successResponses = [];
|
|
151
176
|
for (const [code, response] of Object.entries(responses)) {
|
|
152
|
-
const status =
|
|
153
|
-
if (
|
|
177
|
+
const status = parseResponseStatus(code);
|
|
178
|
+
if (status === undefined || response === undefined) {
|
|
154
179
|
continue;
|
|
155
180
|
}
|
|
156
181
|
const resolved = isReference(response)
|
|
@@ -158,27 +183,46 @@ function pickResponseMetadata(operation, document) {
|
|
|
158
183
|
: response;
|
|
159
184
|
const schema = extractResponseSchema(resolved, document);
|
|
160
185
|
if (schema !== undefined) {
|
|
161
|
-
responseSchemas.
|
|
186
|
+
responseSchemas.push({ ...status, schema });
|
|
187
|
+
}
|
|
188
|
+
if (isSuccessStatus(status.status)) {
|
|
189
|
+
successResponses.push({ status: status.status, response });
|
|
162
190
|
}
|
|
163
191
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
.sort((a, b) => a - b);
|
|
168
|
-
if (successCodes.length === 0) {
|
|
192
|
+
successResponses.sort((a, b) => a.status - b.status);
|
|
193
|
+
const firstSuccess = successResponses[0];
|
|
194
|
+
if (firstSuccess === undefined) {
|
|
169
195
|
return { defaultStatus: 200, defaultExample: undefined, responseSchemas };
|
|
170
196
|
}
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
? resolveReference(response, document)
|
|
175
|
-
: response;
|
|
197
|
+
const resolvedResponse = isReference(firstSuccess.response)
|
|
198
|
+
? resolveReference(firstSuccess.response, document)
|
|
199
|
+
: firstSuccess.response;
|
|
176
200
|
return {
|
|
177
|
-
defaultStatus: status,
|
|
201
|
+
defaultStatus: firstSuccess.status,
|
|
178
202
|
defaultExample: extractExample(resolvedResponse, document),
|
|
179
203
|
responseSchemas
|
|
180
204
|
};
|
|
181
205
|
}
|
|
206
|
+
function parseResponseStatus(code) {
|
|
207
|
+
if (code.length === 3 && code[1]?.toUpperCase() === "X" && code[2]?.toUpperCase() === "X") {
|
|
208
|
+
const family = Number(code[0]);
|
|
209
|
+
return Number.isInteger(family) && family >= 1 && family <= 5
|
|
210
|
+
? { status: family * 100, range: true }
|
|
211
|
+
: undefined;
|
|
212
|
+
}
|
|
213
|
+
if (code.length !== 3 || [...code].some((character) => character < "0" || character > "9")) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
return { status: Number(code), range: false };
|
|
217
|
+
}
|
|
218
|
+
function isSuccessStatus(status) {
|
|
219
|
+
return status >= 200 && status < 300;
|
|
220
|
+
}
|
|
221
|
+
function findResponseSchema(schemas, status) {
|
|
222
|
+
return (schemas.find((candidate) => !candidate.range && candidate.status === status)?.schema ??
|
|
223
|
+
schemas.find((candidate) => candidate.range && Math.floor(status / 100) === candidate.status / 100)
|
|
224
|
+
?.schema);
|
|
225
|
+
}
|
|
182
226
|
function extractResponseSchema(response, document) {
|
|
183
227
|
if (response === undefined) {
|
|
184
228
|
return undefined;
|
|
@@ -211,6 +255,95 @@ function pickRequestBody(operation, document) {
|
|
|
211
255
|
: media.schema;
|
|
212
256
|
return { schema, required: resolved.required === true };
|
|
213
257
|
}
|
|
258
|
+
function collectCompiledParameters(pathParameters, operationParameters, document) {
|
|
259
|
+
const parameters = new Map();
|
|
260
|
+
for (const raw of [...(pathParameters ?? []), ...(operationParameters ?? [])]) {
|
|
261
|
+
const parameter = isReference(raw)
|
|
262
|
+
? resolveReference(raw, document)
|
|
263
|
+
: raw;
|
|
264
|
+
if (parameter.in !== "path" && parameter.in !== "query" && parameter.in !== "header") {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
parameters.set(`${parameter.in}:${parameter.name}`, {
|
|
268
|
+
name: parameter.name,
|
|
269
|
+
location: parameter.in,
|
|
270
|
+
required: parameter.in === "path" || parameter.required === true,
|
|
271
|
+
schema: parameter.schema
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return [...parameters.values()];
|
|
275
|
+
}
|
|
276
|
+
function validateRequestParameters(operation, requestUrl, headers, document) {
|
|
277
|
+
const pathValues = collectPathParameterValues(operation, requestUrl.pathname);
|
|
278
|
+
const errors = [];
|
|
279
|
+
for (const parameter of operation.parameters) {
|
|
280
|
+
const pointer = `$${parameter.location}/${parameter.name}`;
|
|
281
|
+
const value = readParameterValue(parameter, requestUrl, headers, pathValues);
|
|
282
|
+
if (value === undefined) {
|
|
283
|
+
if (parameter.required) {
|
|
284
|
+
errors.push(`${pointer}: required`);
|
|
285
|
+
}
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (parameter.schema !== undefined) {
|
|
289
|
+
errors.push(...validateAgainstSchema(value, parameter.schema, document, pointer));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return errors;
|
|
293
|
+
}
|
|
294
|
+
function collectPathParameterValues(operation, pathname) {
|
|
295
|
+
const match = operation.pathRegex.exec(pathname);
|
|
296
|
+
const values = new Map();
|
|
297
|
+
if (match === null) {
|
|
298
|
+
return values;
|
|
299
|
+
}
|
|
300
|
+
for (let index = 0; index < operation.pathParameterNames.length; index++) {
|
|
301
|
+
const value = match[index + 1];
|
|
302
|
+
if (value !== undefined) {
|
|
303
|
+
values.set(operation.pathParameterNames[index], decodeUrlComponent(value));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return values;
|
|
307
|
+
}
|
|
308
|
+
function readParameterValue(parameter, requestUrl, headers, pathValues) {
|
|
309
|
+
switch (parameter.location) {
|
|
310
|
+
case "path":
|
|
311
|
+
return coerceParameterValue(parameter.schema, pathValues.get(parameter.name));
|
|
312
|
+
case "query": {
|
|
313
|
+
const values = requestUrl.searchParams.getAll(parameter.name);
|
|
314
|
+
if (values.length === 0) {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
return schemaAcceptsArray(parameter.schema) ? coerceParameterValues(values) : values[0];
|
|
318
|
+
}
|
|
319
|
+
case "header":
|
|
320
|
+
return coerceParameterValue(parameter.schema, headers[parameter.name.toLowerCase()]);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function coerceParameterValue(schema, value) {
|
|
324
|
+
if (value === undefined || !schemaAcceptsArray(schema)) {
|
|
325
|
+
return value;
|
|
326
|
+
}
|
|
327
|
+
return coerceParameterValues([value]);
|
|
328
|
+
}
|
|
329
|
+
function coerceParameterValues(values) {
|
|
330
|
+
return values.flatMap((value) => value.split(","));
|
|
331
|
+
}
|
|
332
|
+
function schemaAcceptsArray(schema) {
|
|
333
|
+
if (schema === undefined || isReference(schema)) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
const type = schema.type;
|
|
337
|
+
return Array.isArray(type) ? type.includes("array") : type === "array";
|
|
338
|
+
}
|
|
339
|
+
function decodeUrlComponent(value) {
|
|
340
|
+
try {
|
|
341
|
+
return decodeURIComponent(value);
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
return value;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
214
347
|
function extractExample(response, document) {
|
|
215
348
|
if (response === undefined) {
|
|
216
349
|
return undefined;
|
|
@@ -265,7 +398,7 @@ function resolveReference(reference, document) {
|
|
|
265
398
|
const segments = ref
|
|
266
399
|
.slice(2)
|
|
267
400
|
.split("/")
|
|
268
|
-
.map(
|
|
401
|
+
.map(decodeReferenceSegment);
|
|
269
402
|
let current = document;
|
|
270
403
|
for (const segment of segments) {
|
|
271
404
|
if (current === null || typeof current !== "object") {
|
|
@@ -281,6 +414,16 @@ function resolveReference(reference, document) {
|
|
|
281
414
|
}
|
|
282
415
|
return current;
|
|
283
416
|
}
|
|
417
|
+
function decodeReferenceSegment(segment) {
|
|
418
|
+
let decoded = segment;
|
|
419
|
+
try {
|
|
420
|
+
decoded = decodeURIComponent(segment);
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
decoded = segment;
|
|
424
|
+
}
|
|
425
|
+
return decoded.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
426
|
+
}
|
|
284
427
|
function validateAgainstSchema(value, schema, document, pointer) {
|
|
285
428
|
const resolved = isReference(schema)
|
|
286
429
|
? resolveReference(schema, document)
|
|
@@ -334,11 +477,36 @@ function validateAgainstSchema(value, schema, document, pointer) {
|
|
|
334
477
|
errors.push(...validateAgainstSchema(value[i], resolved.items, document, `${pointer}/${i}`));
|
|
335
478
|
}
|
|
336
479
|
}
|
|
480
|
+
if (typeof value === "number") {
|
|
481
|
+
validateNumberConstraints(value, resolved, pointer, errors);
|
|
482
|
+
}
|
|
483
|
+
if (typeof value === "string") {
|
|
484
|
+
validateStringConstraints(value, resolved, pointer, errors);
|
|
485
|
+
}
|
|
337
486
|
if (resolved.enum !== undefined && !resolved.enum.includes(value)) {
|
|
338
487
|
errors.push(`${pointer}: not in enum`);
|
|
339
488
|
}
|
|
340
489
|
return errors;
|
|
341
490
|
}
|
|
491
|
+
function validateNumberConstraints(value, schema, pointer, errors) {
|
|
492
|
+
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
493
|
+
errors.push(`${pointer}: expected value to be >= ${schema.minimum}`);
|
|
494
|
+
}
|
|
495
|
+
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
496
|
+
errors.push(`${pointer}: expected value to be <= ${schema.maximum}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function validateStringConstraints(value, schema, pointer, errors) {
|
|
500
|
+
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
501
|
+
errors.push(`${pointer}: expected length to be >= ${schema.minLength}`);
|
|
502
|
+
}
|
|
503
|
+
if (schema.maxLength !== undefined && value.length > schema.maxLength) {
|
|
504
|
+
errors.push(`${pointer}: expected length to be <= ${schema.maxLength}`);
|
|
505
|
+
}
|
|
506
|
+
if (schema.pattern !== undefined && !new RegExp(schema.pattern).test(value)) {
|
|
507
|
+
errors.push(`${pointer}: expected value to match pattern ${schema.pattern}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
342
510
|
function normalizeTypes(schema) {
|
|
343
511
|
const type = schema.type;
|
|
344
512
|
if (type === undefined) {
|