toolcraft-openapi 0.0.14 → 0.0.15
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/generate.d.ts +2 -0
- package/dist/generate.js +10 -3
- package/dist/http.js +2 -1
- package/dist/mock/fetch.d.ts +35 -0
- package/dist/mock/fetch.js +498 -0
- package/dist/mock.compile-check.d.ts +1 -0
- package/dist/mock.compile-check.js +15 -0
- package/dist/mock.d.ts +2 -0
- package/dist/mock.js +1 -0
- package/dist/runtime.js +6 -5
- package/package.json +8 -4
package/dist/generate.d.ts
CHANGED
|
@@ -123,6 +123,7 @@ export interface GeneratedParam {
|
|
|
123
123
|
description?: string;
|
|
124
124
|
shortFlag?: string;
|
|
125
125
|
scope?: readonly [GeneratedParamScope, ...GeneratedParamScope[]];
|
|
126
|
+
global?: boolean;
|
|
126
127
|
optional: boolean;
|
|
127
128
|
definition: GeneratedParamDefinition;
|
|
128
129
|
}
|
|
@@ -187,6 +188,7 @@ interface RenderSchemaOptionsInput {
|
|
|
187
188
|
description?: string;
|
|
188
189
|
shortFlag?: string;
|
|
189
190
|
scope?: readonly [GeneratedParamScope, ...GeneratedParamScope[]];
|
|
191
|
+
global?: boolean;
|
|
190
192
|
}
|
|
191
193
|
export type GeneratedPreflightBlock = {
|
|
192
194
|
kind: "scalar-null";
|
package/dist/generate.js
CHANGED
|
@@ -25,6 +25,7 @@ const TRANSPORT_PARAMS = [
|
|
|
25
25
|
location: "transport",
|
|
26
26
|
description: "Print the HTTP request and exit without sending it.",
|
|
27
27
|
scope: ["cli", "sdk"],
|
|
28
|
+
global: true,
|
|
28
29
|
optional: true,
|
|
29
30
|
definition: { kind: "boolean" }
|
|
30
31
|
},
|
|
@@ -35,6 +36,7 @@ const TRANSPORT_PARAMS = [
|
|
|
35
36
|
description: "Log the request line to stderr.",
|
|
36
37
|
shortFlag: "v",
|
|
37
38
|
scope: ["cli", "sdk"],
|
|
39
|
+
global: true,
|
|
38
40
|
optional: true,
|
|
39
41
|
definition: { kind: "boolean" }
|
|
40
42
|
}
|
|
@@ -56,6 +58,10 @@ const SCHEMA_OPTION_SOURCES = [
|
|
|
56
58
|
key: "scope",
|
|
57
59
|
get: (param) => param.scope
|
|
58
60
|
},
|
|
61
|
+
{
|
|
62
|
+
key: "global",
|
|
63
|
+
get: (param) => (param.global === true ? true : undefined)
|
|
64
|
+
},
|
|
59
65
|
{
|
|
60
66
|
key: "minimum",
|
|
61
67
|
get: (param) => param.definition.minimum
|
|
@@ -963,15 +969,16 @@ function renderParamLines(params) {
|
|
|
963
969
|
return params.map((param) => ` ${renderObjectKey(param.paramName)}: ${renderParamSchema(param)},`);
|
|
964
970
|
}
|
|
965
971
|
function renderParamSchema(param) {
|
|
966
|
-
const schema = renderDefinition(param.definition, param.description, param.shortFlag, param.scope);
|
|
972
|
+
const schema = renderDefinition(param.definition, param.description, param.shortFlag, param.scope, param.global);
|
|
967
973
|
return param.optional ? `S.Optional(${schema})` : schema;
|
|
968
974
|
}
|
|
969
|
-
function renderDefinition(definition, description, shortFlag, scope) {
|
|
975
|
+
function renderDefinition(definition, description, shortFlag, scope, global) {
|
|
970
976
|
const options = renderSchemaOptions({
|
|
971
977
|
definition,
|
|
972
978
|
description,
|
|
973
979
|
shortFlag,
|
|
974
|
-
scope
|
|
980
|
+
scope,
|
|
981
|
+
global
|
|
975
982
|
});
|
|
976
983
|
const renderer = DEFINITION_RENDERERS[definition.kind];
|
|
977
984
|
return renderer(definition, options);
|
package/dist/http.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { text as designText } from "@poe-code/design-system";
|
|
1
2
|
import { UserError } from "toolcraft";
|
|
2
3
|
export class HttpError extends Error {
|
|
3
4
|
status;
|
|
@@ -20,7 +21,7 @@ export async function requestJson(options) {
|
|
|
20
21
|
const writeStderr = options.writeStderr ?? process.stderr.write.bind(process.stderr);
|
|
21
22
|
const requestLine = `${method} ${url}`;
|
|
22
23
|
if (options.verbose) {
|
|
23
|
-
writeStderr(`${requestLine}\n`);
|
|
24
|
+
writeStderr(`${designText.muted(requestLine)}\n`);
|
|
24
25
|
}
|
|
25
26
|
if (options.dryRun) {
|
|
26
27
|
writeStdout(formatDryRunOutput(requestLine, headers, options.body));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { OpenApiDocument } from "../generate.js";
|
|
2
|
+
import { type OpenApiSourceFileSystem } from "../spec-source.js";
|
|
3
|
+
export type OnUnmocked = "throw" | "reply404";
|
|
4
|
+
export interface MockFixtureEntry {
|
|
5
|
+
status?: number;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
body?: unknown;
|
|
8
|
+
}
|
|
9
|
+
export type MockFetchFixtures = Record<string, MockFixtureEntry> | string;
|
|
10
|
+
export interface MockFetchFileSystem extends OpenApiSourceFileSystem {
|
|
11
|
+
readdir?(directory: string): Promise<string[]>;
|
|
12
|
+
}
|
|
13
|
+
export interface MockFetchOptions {
|
|
14
|
+
spec: OpenApiDocument | string | URL;
|
|
15
|
+
fixtures?: MockFetchFixtures;
|
|
16
|
+
onUnmocked?: OnUnmocked;
|
|
17
|
+
cwd?: string;
|
|
18
|
+
fs?: MockFetchFileSystem;
|
|
19
|
+
fetch?: typeof globalThis.fetch;
|
|
20
|
+
}
|
|
21
|
+
export interface RequestRecord {
|
|
22
|
+
method: string;
|
|
23
|
+
path: string;
|
|
24
|
+
operationId: string;
|
|
25
|
+
headers: Record<string, string>;
|
|
26
|
+
body: unknown;
|
|
27
|
+
at: Date;
|
|
28
|
+
}
|
|
29
|
+
export interface MockFetchHandle {
|
|
30
|
+
fetch: typeof globalThis.fetch;
|
|
31
|
+
requests: RequestRecord[];
|
|
32
|
+
reset(): void;
|
|
33
|
+
}
|
|
34
|
+
export declare function mockFetch(options: MockFetchOptions): Promise<MockFetchHandle>;
|
|
35
|
+
export type { OpenApiSourceFileSystem };
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { UserError } from "toolcraft";
|
|
4
|
+
import { parseOpenApiDocument, readOpenApiSourceText } from "../spec-source.js";
|
|
5
|
+
const HTTP_METHOD_NAMES = ["get", "post", "put", "patch", "delete"];
|
|
6
|
+
export async function mockFetch(options) {
|
|
7
|
+
const document = await resolveSpec(options);
|
|
8
|
+
const operations = compileOperations(document);
|
|
9
|
+
const operationIds = new Set(operations.map((op) => op.operationId));
|
|
10
|
+
const fixtureLoader = await createFixtureLoader(options.fixtures, options.cwd, options.fs, operationIds);
|
|
11
|
+
const onUnmocked = options.onUnmocked ?? "throw";
|
|
12
|
+
const requests = [];
|
|
13
|
+
const fetchImpl = async (input, init) => {
|
|
14
|
+
const requestUrl = parseRequestUrl(input);
|
|
15
|
+
const method = (init?.method ?? getRequestMethod(input) ?? "GET").toUpperCase();
|
|
16
|
+
const headers = collectHeaders(input, init);
|
|
17
|
+
const bodyText = await readRequestBody(input, init);
|
|
18
|
+
const parsedBody = parseJsonOrUndefined(bodyText);
|
|
19
|
+
const matchingPath = operations.filter((op) => op.pathRegex.test(requestUrl.pathname));
|
|
20
|
+
if (matchingPath.length === 0) {
|
|
21
|
+
throw new MockFetchError(`mockFetch: no operation in the spec matches ${method} ${requestUrl.pathname}.`);
|
|
22
|
+
}
|
|
23
|
+
const operation = matchingPath.find((op) => op.method === method);
|
|
24
|
+
if (operation === undefined) {
|
|
25
|
+
const allowed = matchingPath.map((op) => op.method).join(", ");
|
|
26
|
+
throw new MockFetchError(`mockFetch: method ${method} not declared for ${requestUrl.pathname} (spec allows ${allowed}).`);
|
|
27
|
+
}
|
|
28
|
+
requests.push({
|
|
29
|
+
method,
|
|
30
|
+
path: requestUrl.pathname,
|
|
31
|
+
operationId: operation.operationId,
|
|
32
|
+
headers,
|
|
33
|
+
body: parsedBody,
|
|
34
|
+
at: new Date()
|
|
35
|
+
});
|
|
36
|
+
if (operation.requestBodySchema !== undefined && parsedBody !== undefined) {
|
|
37
|
+
const errors = validateAgainstSchema(parsedBody, operation.requestBodySchema, document, "$");
|
|
38
|
+
if (errors.length > 0) {
|
|
39
|
+
return jsonResponse(422, { errors });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else if (operation.requestBodyRequired && parsedBody === undefined) {
|
|
43
|
+
return jsonResponse(422, { errors: ["$: request body is required"] });
|
|
44
|
+
}
|
|
45
|
+
const fixture = await fixtureLoader(operation.operationId);
|
|
46
|
+
if (fixture !== undefined) {
|
|
47
|
+
const status = fixture.status ?? operation.defaultStatus;
|
|
48
|
+
const responseSchema = operation.responseSchemas.get(status);
|
|
49
|
+
if (responseSchema !== undefined && fixture.body !== undefined) {
|
|
50
|
+
const errors = validateAgainstSchema(fixture.body, responseSchema, document, "$response");
|
|
51
|
+
if (errors.length > 0) {
|
|
52
|
+
throw new MockFetchError(`mockFetch: response fixture for ${JSON.stringify(operation.operationId)} ` +
|
|
53
|
+
`violates the spec response schema for status ${status}:\n ${errors.join("\n ")}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return buildResponse(fixture, operation.defaultStatus);
|
|
57
|
+
}
|
|
58
|
+
if (operation.defaultExample !== undefined) {
|
|
59
|
+
return buildResponse({ body: operation.defaultExample }, operation.defaultStatus);
|
|
60
|
+
}
|
|
61
|
+
if (onUnmocked === "reply404") {
|
|
62
|
+
return jsonResponse(404, { error: `unmocked: ${operation.operationId}` });
|
|
63
|
+
}
|
|
64
|
+
throw new MockFetchError(`mockFetch: unmocked operation ${JSON.stringify(operation.operationId)}. ` +
|
|
65
|
+
`Add a fixture, an OpenAPI example on the success response, or pass { onUnmocked: "reply404" }.`);
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
fetch: fetchImpl,
|
|
69
|
+
requests,
|
|
70
|
+
reset() {
|
|
71
|
+
requests.length = 0;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
class MockFetchError extends Error {
|
|
76
|
+
constructor(message) {
|
|
77
|
+
super(message);
|
|
78
|
+
this.name = "MockFetchError";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function resolveSpec(options) {
|
|
82
|
+
const { spec } = options;
|
|
83
|
+
if (typeof spec !== "string" && !(spec instanceof URL)) {
|
|
84
|
+
return spec;
|
|
85
|
+
}
|
|
86
|
+
const sourceText = await readOpenApiSourceText(spec, {
|
|
87
|
+
cwd: options.cwd ?? process.cwd(),
|
|
88
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
89
|
+
fs: options.fs ?? fs
|
|
90
|
+
});
|
|
91
|
+
return parseOpenApiDocument(sourceText, spec);
|
|
92
|
+
}
|
|
93
|
+
function compileOperations(document) {
|
|
94
|
+
const paths = document.paths;
|
|
95
|
+
if (paths === undefined) {
|
|
96
|
+
throw new UserError('mockFetch: OpenAPI document must define a top-level "paths" object.');
|
|
97
|
+
}
|
|
98
|
+
const compiled = [];
|
|
99
|
+
for (const [pathTemplate, pathItem] of Object.entries(paths)) {
|
|
100
|
+
if (pathItem === undefined) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
for (const method of HTTP_METHOD_NAMES) {
|
|
104
|
+
const operation = pathItem[method];
|
|
105
|
+
if (operation === undefined) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const resolvedOperation = resolveOperation(operation, document);
|
|
109
|
+
const operationId = resolvedOperation.operationId ?? `${method.toUpperCase()} ${pathTemplate}`;
|
|
110
|
+
const { defaultStatus, defaultExample, responseSchemas } = pickResponseMetadata(resolvedOperation, document);
|
|
111
|
+
const { schema: requestBodySchema, required: requestBodyRequired } = pickRequestBody(resolvedOperation, document);
|
|
112
|
+
compiled.push({
|
|
113
|
+
method: method.toUpperCase(),
|
|
114
|
+
pathTemplate,
|
|
115
|
+
pathRegex: pathTemplateToRegex(pathTemplate),
|
|
116
|
+
pathSpecificity: countPathPlaceholders(pathTemplate),
|
|
117
|
+
operationId,
|
|
118
|
+
operation: resolvedOperation,
|
|
119
|
+
defaultStatus,
|
|
120
|
+
defaultExample,
|
|
121
|
+
requestBodySchema,
|
|
122
|
+
requestBodyRequired,
|
|
123
|
+
responseSchemas
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Literal paths win over templated paths when both match the same pathname.
|
|
128
|
+
// Sort ascending by placeholder count so concrete operations are matched first.
|
|
129
|
+
compiled.sort((a, b) => a.pathSpecificity - b.pathSpecificity);
|
|
130
|
+
return compiled;
|
|
131
|
+
}
|
|
132
|
+
function countPathPlaceholders(template) {
|
|
133
|
+
return (template.match(/\{[^}]+\}/g) ?? []).length;
|
|
134
|
+
}
|
|
135
|
+
function pathTemplateToRegex(template) {
|
|
136
|
+
// Escape regex metacharacters except for "{...}" placeholders, which become non-slash captures.
|
|
137
|
+
const pattern = template.replace(/[.*+?^${}()|[\]\\]/g, (match) => match === "{" || match === "}" ? match : `\\${match}`);
|
|
138
|
+
const withParams = pattern.replace(/\{[^}]+\}/g, "[^/]+");
|
|
139
|
+
return new RegExp(`^${withParams}$`);
|
|
140
|
+
}
|
|
141
|
+
function resolveOperation(operation, document) {
|
|
142
|
+
if (isReference(operation)) {
|
|
143
|
+
return resolveReference(operation, document);
|
|
144
|
+
}
|
|
145
|
+
return operation;
|
|
146
|
+
}
|
|
147
|
+
function pickResponseMetadata(operation, document) {
|
|
148
|
+
const responses = operation.responses ?? {};
|
|
149
|
+
const responseSchemas = new Map();
|
|
150
|
+
for (const [code, response] of Object.entries(responses)) {
|
|
151
|
+
const status = parseInt(code, 10);
|
|
152
|
+
if (!Number.isFinite(status) || response === undefined) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const resolved = isReference(response)
|
|
156
|
+
? resolveReference(response, document)
|
|
157
|
+
: response;
|
|
158
|
+
const schema = extractResponseSchema(resolved, document);
|
|
159
|
+
if (schema !== undefined) {
|
|
160
|
+
responseSchemas.set(status, schema);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const successCodes = Object.keys(responses)
|
|
164
|
+
.map((code) => parseInt(code, 10))
|
|
165
|
+
.filter((code) => Number.isFinite(code) && code >= 200 && code < 300)
|
|
166
|
+
.sort((a, b) => a - b);
|
|
167
|
+
if (successCodes.length === 0) {
|
|
168
|
+
return { defaultStatus: 200, defaultExample: undefined, responseSchemas };
|
|
169
|
+
}
|
|
170
|
+
const status = successCodes[0];
|
|
171
|
+
const response = responses[String(status)];
|
|
172
|
+
const resolvedResponse = response !== undefined && isReference(response)
|
|
173
|
+
? resolveReference(response, document)
|
|
174
|
+
: response;
|
|
175
|
+
return {
|
|
176
|
+
defaultStatus: status,
|
|
177
|
+
defaultExample: extractExample(resolvedResponse, document),
|
|
178
|
+
responseSchemas
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function extractResponseSchema(response, document) {
|
|
182
|
+
if (response === undefined) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
const media = pickJsonMediaType(response.content);
|
|
186
|
+
if (media === undefined || media.schema === undefined) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
return isReference(media.schema)
|
|
190
|
+
? resolveReference(media.schema, document)
|
|
191
|
+
: media.schema;
|
|
192
|
+
}
|
|
193
|
+
function pickRequestBody(operation, document) {
|
|
194
|
+
const raw = operation.requestBody;
|
|
195
|
+
if (raw === undefined) {
|
|
196
|
+
return { schema: undefined, required: false };
|
|
197
|
+
}
|
|
198
|
+
const resolved = isReference(raw)
|
|
199
|
+
? resolveReference(raw, document)
|
|
200
|
+
: raw;
|
|
201
|
+
const media = pickJsonMediaType(resolved.content);
|
|
202
|
+
if (media === undefined) {
|
|
203
|
+
return { schema: undefined, required: resolved.required === true };
|
|
204
|
+
}
|
|
205
|
+
if (media.schema === undefined) {
|
|
206
|
+
return { schema: undefined, required: resolved.required === true };
|
|
207
|
+
}
|
|
208
|
+
const schema = isReference(media.schema)
|
|
209
|
+
? resolveReference(media.schema, document)
|
|
210
|
+
: media.schema;
|
|
211
|
+
return { schema, required: resolved.required === true };
|
|
212
|
+
}
|
|
213
|
+
function extractExample(response, document) {
|
|
214
|
+
if (response === undefined) {
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
const media = pickJsonMediaType(response.content);
|
|
218
|
+
if (media === undefined) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
if (media.example !== undefined) {
|
|
222
|
+
return media.example;
|
|
223
|
+
}
|
|
224
|
+
if (media.examples !== undefined) {
|
|
225
|
+
for (const value of Object.values(media.examples)) {
|
|
226
|
+
if (value !== undefined && "value" in value) {
|
|
227
|
+
return value.value;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (media.schema !== undefined) {
|
|
232
|
+
const schema = isReference(media.schema)
|
|
233
|
+
? resolveReference(media.schema, document)
|
|
234
|
+
: media.schema;
|
|
235
|
+
const schemaExample = schema.example;
|
|
236
|
+
if (schemaExample !== undefined) {
|
|
237
|
+
return schemaExample;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
function pickJsonMediaType(content) {
|
|
243
|
+
if (content === undefined) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
for (const [type, media] of Object.entries(content)) {
|
|
247
|
+
if (media !== undefined && /application\/json|\+json/i.test(type)) {
|
|
248
|
+
return media;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
function isReference(value) {
|
|
254
|
+
return (typeof value === "object" &&
|
|
255
|
+
value !== null &&
|
|
256
|
+
"$ref" in value &&
|
|
257
|
+
typeof value.$ref === "string");
|
|
258
|
+
}
|
|
259
|
+
function resolveReference(reference, document) {
|
|
260
|
+
const ref = reference.$ref;
|
|
261
|
+
if (!ref.startsWith("#/")) {
|
|
262
|
+
throw new UserError(`mockFetch: only local $ref values are supported, got ${JSON.stringify(ref)}.`);
|
|
263
|
+
}
|
|
264
|
+
const segments = ref
|
|
265
|
+
.slice(2)
|
|
266
|
+
.split("/")
|
|
267
|
+
.map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
268
|
+
let current = document;
|
|
269
|
+
for (const segment of segments) {
|
|
270
|
+
if (current === null || typeof current !== "object") {
|
|
271
|
+
throw new UserError(`mockFetch: failed to resolve $ref ${JSON.stringify(ref)}.`);
|
|
272
|
+
}
|
|
273
|
+
current = current[segment];
|
|
274
|
+
}
|
|
275
|
+
if (current === undefined) {
|
|
276
|
+
throw new UserError(`mockFetch: failed to resolve $ref ${JSON.stringify(ref)}.`);
|
|
277
|
+
}
|
|
278
|
+
return current;
|
|
279
|
+
}
|
|
280
|
+
function validateAgainstSchema(value, schema, document, pointer) {
|
|
281
|
+
const resolved = isReference(schema)
|
|
282
|
+
? resolveReference(schema, document)
|
|
283
|
+
: schema;
|
|
284
|
+
if (resolved.anyOf !== undefined) {
|
|
285
|
+
return resolved.anyOf.some((branch) => validateAgainstSchema(value, branch, document, pointer).length === 0)
|
|
286
|
+
? []
|
|
287
|
+
: [`${pointer}: did not match any anyOf branch`];
|
|
288
|
+
}
|
|
289
|
+
if (resolved.oneOf !== undefined) {
|
|
290
|
+
const matches = resolved.oneOf.filter((branch) => validateAgainstSchema(value, branch, document, pointer).length === 0);
|
|
291
|
+
return matches.length === 1 ? [] : [`${pointer}: matched ${matches.length} oneOf branches`];
|
|
292
|
+
}
|
|
293
|
+
const errors = [];
|
|
294
|
+
if (resolved.allOf !== undefined) {
|
|
295
|
+
for (const branch of resolved.allOf) {
|
|
296
|
+
errors.push(...validateAgainstSchema(value, branch, document, pointer));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const types = normalizeTypes(resolved);
|
|
300
|
+
if (types.length > 0 && !types.some((type) => matchesPrimitiveType(value, type))) {
|
|
301
|
+
errors.push(`${pointer}: expected ${types.join(" or ")}, got ${describeValue(value)}`);
|
|
302
|
+
return errors;
|
|
303
|
+
}
|
|
304
|
+
if (types.includes("object") && typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
305
|
+
for (const required of resolved.required ?? []) {
|
|
306
|
+
if (!(required in value)) {
|
|
307
|
+
errors.push(`${pointer}/${required}: required`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (resolved.properties !== undefined) {
|
|
311
|
+
for (const [key, propValue] of Object.entries(value)) {
|
|
312
|
+
const propSchema = resolved.properties[key];
|
|
313
|
+
if (propSchema !== undefined) {
|
|
314
|
+
errors.push(...validateAgainstSchema(propValue, propSchema, document, `${pointer}/${key}`));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (types.includes("array") && Array.isArray(value) && resolved.items !== undefined) {
|
|
320
|
+
for (let i = 0; i < value.length; i++) {
|
|
321
|
+
errors.push(...validateAgainstSchema(value[i], resolved.items, document, `${pointer}/${i}`));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (resolved.enum !== undefined && !resolved.enum.includes(value)) {
|
|
325
|
+
errors.push(`${pointer}: not in enum`);
|
|
326
|
+
}
|
|
327
|
+
return errors;
|
|
328
|
+
}
|
|
329
|
+
function normalizeTypes(schema) {
|
|
330
|
+
const type = schema.type;
|
|
331
|
+
if (type === undefined) {
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
const list = Array.isArray(type) ? type.slice() : [type];
|
|
335
|
+
if (schema.nullable === true && !list.includes("null")) {
|
|
336
|
+
list.push("null");
|
|
337
|
+
}
|
|
338
|
+
return list;
|
|
339
|
+
}
|
|
340
|
+
function matchesPrimitiveType(value, type) {
|
|
341
|
+
switch (type) {
|
|
342
|
+
case "object":
|
|
343
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
344
|
+
case "array":
|
|
345
|
+
return Array.isArray(value);
|
|
346
|
+
case "string":
|
|
347
|
+
return typeof value === "string";
|
|
348
|
+
case "number":
|
|
349
|
+
return typeof value === "number" && !Number.isNaN(value);
|
|
350
|
+
case "integer":
|
|
351
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
352
|
+
case "boolean":
|
|
353
|
+
return typeof value === "boolean";
|
|
354
|
+
case "null":
|
|
355
|
+
return value === null;
|
|
356
|
+
default:
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function describeValue(value) {
|
|
361
|
+
if (value === null)
|
|
362
|
+
return "null";
|
|
363
|
+
if (Array.isArray(value))
|
|
364
|
+
return "array";
|
|
365
|
+
return typeof value;
|
|
366
|
+
}
|
|
367
|
+
function parseRequestUrl(input) {
|
|
368
|
+
if (input instanceof URL) {
|
|
369
|
+
return input;
|
|
370
|
+
}
|
|
371
|
+
if (typeof input === "string") {
|
|
372
|
+
return new URL(input);
|
|
373
|
+
}
|
|
374
|
+
return new URL(input.url);
|
|
375
|
+
}
|
|
376
|
+
function getRequestMethod(input) {
|
|
377
|
+
if (typeof input === "string" || input instanceof URL) {
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
return input.method;
|
|
381
|
+
}
|
|
382
|
+
function collectHeaders(input, init) {
|
|
383
|
+
const headers = {};
|
|
384
|
+
const initHeaders = init?.headers;
|
|
385
|
+
const requestHeaders = typeof input !== "string" && !(input instanceof URL) ? input.headers : undefined;
|
|
386
|
+
appendHeaders(headers, requestHeaders);
|
|
387
|
+
appendHeaders(headers, initHeaders);
|
|
388
|
+
return headers;
|
|
389
|
+
}
|
|
390
|
+
function appendHeaders(target, source) {
|
|
391
|
+
if (source === undefined) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (source instanceof Headers) {
|
|
395
|
+
source.forEach((value, key) => {
|
|
396
|
+
target[key.toLowerCase()] = value;
|
|
397
|
+
});
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (Array.isArray(source)) {
|
|
401
|
+
for (const [key, value] of source) {
|
|
402
|
+
target[key.toLowerCase()] = value;
|
|
403
|
+
}
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
for (const [key, value] of Object.entries(source)) {
|
|
407
|
+
target[key.toLowerCase()] = String(value);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function readRequestBody(input, init) {
|
|
411
|
+
if (init?.body !== undefined && init.body !== null) {
|
|
412
|
+
return typeof init.body === "string" ? init.body : await new Response(init.body).text();
|
|
413
|
+
}
|
|
414
|
+
if (typeof input !== "string" && !(input instanceof URL)) {
|
|
415
|
+
return await input.clone().text();
|
|
416
|
+
}
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
function parseJsonOrUndefined(text) {
|
|
420
|
+
if (text === undefined || text.length === 0) {
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
return JSON.parse(text);
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
return text;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function buildResponse(fixture, defaultStatus) {
|
|
431
|
+
const status = fixture.status ?? defaultStatus;
|
|
432
|
+
const headers = new Headers(fixture.headers ?? { "content-type": "application/json" });
|
|
433
|
+
const body = fixture.body === undefined ? null : fixture.body === null ? null : JSON.stringify(fixture.body);
|
|
434
|
+
return new Response(body, { status, headers });
|
|
435
|
+
}
|
|
436
|
+
function jsonResponse(status, body) {
|
|
437
|
+
return new Response(JSON.stringify(body), {
|
|
438
|
+
status,
|
|
439
|
+
headers: { "content-type": "application/json" }
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
async function createFixtureLoader(fixtures, cwd, injectedFs, operationIds) {
|
|
443
|
+
if (fixtures === undefined) {
|
|
444
|
+
return async () => undefined;
|
|
445
|
+
}
|
|
446
|
+
if (typeof fixtures !== "string") {
|
|
447
|
+
rejectUnknownFixtureKeys(Object.keys(fixtures), operationIds, "fixture key");
|
|
448
|
+
const map = fixtures;
|
|
449
|
+
return async (operationId) => map[operationId];
|
|
450
|
+
}
|
|
451
|
+
const directory = path.resolve(cwd ?? process.cwd(), fixtures);
|
|
452
|
+
const fileSystem = injectedFs ?? fs;
|
|
453
|
+
const readdir = fileSystem.readdir?.bind(fileSystem);
|
|
454
|
+
if (readdir === undefined) {
|
|
455
|
+
throw new UserError("mockFetch: directory fixtures require an fs implementation that exposes readdir.");
|
|
456
|
+
}
|
|
457
|
+
let entries;
|
|
458
|
+
try {
|
|
459
|
+
entries = await readdir(directory);
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
if (isNotFoundError(error)) {
|
|
463
|
+
return async () => undefined;
|
|
464
|
+
}
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
const fixtureFiles = entries.filter((entry) => entry.endsWith(".json"));
|
|
468
|
+
const operationIdsFromFiles = fixtureFiles.map((entry) => entry.slice(0, -".json".length));
|
|
469
|
+
rejectUnknownFixtureKeys(operationIdsFromFiles, operationIds, "fixture file");
|
|
470
|
+
const cache = new Map();
|
|
471
|
+
for (const entry of fixtureFiles) {
|
|
472
|
+
const operationId = entry.slice(0, -".json".length);
|
|
473
|
+
const filePath = path.join(directory, entry);
|
|
474
|
+
const contents = await fileSystem.readFile(filePath, "utf8");
|
|
475
|
+
cache.set(operationId, JSON.parse(contents));
|
|
476
|
+
}
|
|
477
|
+
return async (operationId) => cache.get(operationId);
|
|
478
|
+
}
|
|
479
|
+
function rejectUnknownFixtureKeys(candidates, operationIds, label) {
|
|
480
|
+
const unknown = candidates.filter((key) => !operationIds.has(key));
|
|
481
|
+
if (unknown.length === 0) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const sorted = [...operationIds].sort();
|
|
485
|
+
throw new UserError(`mockFetch: ${unknown.length === 1 ? label : `${label}s`} ${formatList(unknown)} ` +
|
|
486
|
+
`${unknown.length === 1 ? "is" : "are"} not declared in the spec. ` +
|
|
487
|
+
`Known operationIds: ${sorted.length === 0 ? "(none)" : formatList(sorted)}.`);
|
|
488
|
+
}
|
|
489
|
+
function formatList(values) {
|
|
490
|
+
return values.map((value) => JSON.stringify(value)).join(", ");
|
|
491
|
+
}
|
|
492
|
+
function isNotFoundError(error) {
|
|
493
|
+
if (typeof error !== "object" || error === null) {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
const code = error.code;
|
|
497
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
498
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { mockFetch } from "./mock.js";
|
|
2
|
+
const ignoredHandlePromise = mockFetch({
|
|
3
|
+
spec: { openapi: "3.0.0", info: { title: "T", version: "0" }, paths: {} },
|
|
4
|
+
fixtures: { whoami: { body: { handle: "x" } } },
|
|
5
|
+
onUnmocked: "throw"
|
|
6
|
+
});
|
|
7
|
+
void ignoredHandlePromise.then((handle) => {
|
|
8
|
+
void handle.fetch;
|
|
9
|
+
void handle.requests;
|
|
10
|
+
void handle.reset;
|
|
11
|
+
});
|
|
12
|
+
const ignoredOptions = { spec: "./openapi.json" };
|
|
13
|
+
void ignoredOptions;
|
|
14
|
+
const ignoredFixture = { status: 200, body: { ok: true } };
|
|
15
|
+
void ignoredFixture;
|
package/dist/mock.d.ts
ADDED
package/dist/mock.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { mockFetch } from "./mock/fetch.js";
|
package/dist/runtime.js
CHANGED
|
@@ -66,11 +66,11 @@ function createRuntimeHandler(command) {
|
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
function createRuntimeParamSchema(param) {
|
|
69
|
-
const definition = createRuntimeDefinition(param.definition, param.description, param.shortFlag, param.scope);
|
|
69
|
+
const definition = createRuntimeDefinition(param.definition, param.description, param.shortFlag, param.scope, param.global);
|
|
70
70
|
return param.optional ? S.Optional(definition) : definition;
|
|
71
71
|
}
|
|
72
|
-
function createRuntimeDefinition(definition, description, shortFlag, scope) {
|
|
73
|
-
const options = createRuntimeSchemaOptions(definition, description, shortFlag, scope);
|
|
72
|
+
function createRuntimeDefinition(definition, description, shortFlag, scope, global) {
|
|
73
|
+
const options = createRuntimeSchemaOptions(definition, description, shortFlag, scope, global);
|
|
74
74
|
return RUNTIME_DEFINITION_BUILDERS[definition.kind](definition, options);
|
|
75
75
|
}
|
|
76
76
|
const RUNTIME_DEFINITION_BUILDERS = {
|
|
@@ -83,12 +83,13 @@ const RUNTIME_DEFINITION_BUILDERS = {
|
|
|
83
83
|
number: (_definition, options) => options === undefined ? S.Number() : S.Number(options),
|
|
84
84
|
string: (_definition, options) => options === undefined ? S.String() : S.String(options)
|
|
85
85
|
};
|
|
86
|
-
function createRuntimeSchemaOptions(definition, description, shortFlag, scope) {
|
|
86
|
+
function createRuntimeSchemaOptions(definition, description, shortFlag, scope, global) {
|
|
87
87
|
const options = Object.fromEntries(collectSchemaOptionEntries({
|
|
88
88
|
definition,
|
|
89
89
|
description,
|
|
90
90
|
shortFlag,
|
|
91
|
-
scope
|
|
91
|
+
scope,
|
|
92
|
+
global
|
|
92
93
|
}).map(({ key, value }) => [key, Array.isArray(value) ? [...value] : value]));
|
|
93
94
|
return Object.keys(options).length === 0 ? undefined : options;
|
|
94
95
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolcraft-openapi",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -8,12 +8,16 @@
|
|
|
8
8
|
".": {
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
10
10
|
"import": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./mock": {
|
|
13
|
+
"types": "./dist/mock.d.ts",
|
|
14
|
+
"import": "./dist/mock.js"
|
|
11
15
|
}
|
|
12
16
|
},
|
|
13
17
|
"scripts": {
|
|
14
18
|
"build": "rm -rf dist && tsc",
|
|
15
|
-
"test": "cd ../.. && vitest run packages/toolcraft-openapi/src
|
|
16
|
-
"test:unit": "cd ../.. && vitest run packages/toolcraft-openapi/src
|
|
19
|
+
"test": "cd ../.. && vitest run packages/toolcraft-openapi/src",
|
|
20
|
+
"test:unit": "cd ../.. && vitest run packages/toolcraft-openapi/src",
|
|
17
21
|
"prepack": "node ../../scripts/manage-bundled-workspace-deps.mjs prepare . @poe-code/design-system auth-store",
|
|
18
22
|
"postpack": "node ../../scripts/manage-bundled-workspace-deps.mjs cleanup . @poe-code/design-system auth-store"
|
|
19
23
|
},
|
|
@@ -30,7 +34,7 @@
|
|
|
30
34
|
"auth-store": "^0.0.1",
|
|
31
35
|
"chalk": "^5.6.2",
|
|
32
36
|
"console-table-printer": "^2.15.0",
|
|
33
|
-
"toolcraft": "^0.0.
|
|
37
|
+
"toolcraft": "^0.0.15",
|
|
34
38
|
"yaml": "^2.8.2"
|
|
35
39
|
},
|
|
36
40
|
"engines": {
|