@smithers-orchestrator/openapi 0.16.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/LICENSE +21 -0
- package/package.json +38 -0
- package/src/HttpMethod.ts +1 -0
- package/src/MediaTypeObject.ts +6 -0
- package/src/OpenApiAuth.ts +16 -0
- package/src/OpenApiSpec.ts +23 -0
- package/src/OpenApiTool.ts +8 -0
- package/src/OpenApiToolsOptions.ts +10 -0
- package/src/OperationObject.ts +14 -0
- package/src/ParameterObject.ts +11 -0
- package/src/ParsedOperation.ts +14 -0
- package/src/PathItem.ts +12 -0
- package/src/RefObject.ts +3 -0
- package/src/RequestBodyObject.ts +7 -0
- package/src/SchemaObject.ts +23 -0
- package/src/_specHelpers.js +65 -0
- package/src/buildOperationSchema.js +63 -0
- package/src/extractOperations.js +50 -0
- package/src/index.d.ts +287 -0
- package/src/index.js +17 -0
- package/src/jsonSchemaToZod.js +197 -0
- package/src/loadSpecEffect.js +51 -0
- package/src/loadSpecSync.js +27 -0
- package/src/metrics.js +19 -0
- package/src/ref-resolver.js +58 -0
- package/src/schema-converter.js +2 -0
- package/src/spec-parser.js +3 -0
- package/src/tool-factory/_helpers.js +237 -0
- package/src/tool-factory/createOpenApiTool.js +22 -0
- package/src/tool-factory/createOpenApiToolSync.js +22 -0
- package/src/tool-factory/createOpenApiTools.js +21 -0
- package/src/tool-factory/createOpenApiToolsSync.js +20 -0
- package/src/tool-factory/index.js +5 -0
- package/src/tool-factory/listOperations.js +23 -0
- package/src/tool-factory.js +1 -0
- package/src/types.js +20 -0
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import * as effect from 'effect';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { Tool } from 'ai';
|
|
5
|
+
import * as effect_MetricState from 'effect/MetricState';
|
|
6
|
+
import * as effect_MetricKeyType from 'effect/MetricKeyType';
|
|
7
|
+
|
|
8
|
+
type HttpMethod = "get" | "post" | "put" | "delete" | "patch";
|
|
9
|
+
|
|
10
|
+
type RefObject$1 = {
|
|
11
|
+
$ref: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type SchemaObject$1 = {
|
|
15
|
+
type?: string;
|
|
16
|
+
format?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
properties?: Record<string, SchemaObject$1 | RefObject$1>;
|
|
19
|
+
required?: string[];
|
|
20
|
+
items?: SchemaObject$1 | RefObject$1;
|
|
21
|
+
enum?: unknown[];
|
|
22
|
+
default?: unknown;
|
|
23
|
+
nullable?: boolean;
|
|
24
|
+
oneOf?: Array<SchemaObject$1 | RefObject$1>;
|
|
25
|
+
anyOf?: Array<SchemaObject$1 | RefObject$1>;
|
|
26
|
+
allOf?: Array<SchemaObject$1 | RefObject$1>;
|
|
27
|
+
additionalProperties?: boolean | SchemaObject$1 | RefObject$1;
|
|
28
|
+
minimum?: number;
|
|
29
|
+
maximum?: number;
|
|
30
|
+
minLength?: number;
|
|
31
|
+
maxLength?: number;
|
|
32
|
+
pattern?: string;
|
|
33
|
+
$ref?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type ParameterObject$1 = {
|
|
37
|
+
name: string;
|
|
38
|
+
in: "query" | "header" | "path" | "cookie";
|
|
39
|
+
description?: string;
|
|
40
|
+
required?: boolean;
|
|
41
|
+
schema?: SchemaObject$1 | RefObject$1;
|
|
42
|
+
deprecated?: boolean;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type MediaTypeObject = {
|
|
46
|
+
schema?: SchemaObject$1 | RefObject$1;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type RequestBodyObject$1 = {
|
|
50
|
+
description?: string;
|
|
51
|
+
required?: boolean;
|
|
52
|
+
content: Record<string, MediaTypeObject>;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type ParsedOperation$2 = {
|
|
56
|
+
operationId: string;
|
|
57
|
+
method: HttpMethod;
|
|
58
|
+
path: string;
|
|
59
|
+
summary: string;
|
|
60
|
+
description: string;
|
|
61
|
+
parameters: ParameterObject$1[];
|
|
62
|
+
requestBody?: RequestBodyObject$1;
|
|
63
|
+
deprecated: boolean;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type OpenApiAuth$1 = {
|
|
67
|
+
type: "bearer";
|
|
68
|
+
token: string;
|
|
69
|
+
} | {
|
|
70
|
+
type: "basic";
|
|
71
|
+
username: string;
|
|
72
|
+
password: string;
|
|
73
|
+
} | {
|
|
74
|
+
type: "apiKey";
|
|
75
|
+
name: string;
|
|
76
|
+
value: string;
|
|
77
|
+
in: "header" | "query";
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type OpenApiToolsOptions$5 = {
|
|
81
|
+
baseUrl?: string;
|
|
82
|
+
headers?: Record<string, string>;
|
|
83
|
+
auth?: OpenApiAuth$1;
|
|
84
|
+
include?: string[];
|
|
85
|
+
exclude?: string[];
|
|
86
|
+
namePrefix?: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
type OperationObject = {
|
|
90
|
+
operationId?: string;
|
|
91
|
+
summary?: string;
|
|
92
|
+
description?: string;
|
|
93
|
+
parameters?: Array<ParameterObject$1 | RefObject$1>;
|
|
94
|
+
requestBody?: RequestBodyObject$1 | RefObject$1;
|
|
95
|
+
responses?: Record<string, unknown>;
|
|
96
|
+
tags?: string[];
|
|
97
|
+
deprecated?: boolean;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
type PathItem = {
|
|
101
|
+
get?: OperationObject;
|
|
102
|
+
post?: OperationObject;
|
|
103
|
+
put?: OperationObject;
|
|
104
|
+
delete?: OperationObject;
|
|
105
|
+
patch?: OperationObject;
|
|
106
|
+
parameters?: Array<ParameterObject$1 | RefObject$1>;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
type OpenApiSpec$b = {
|
|
110
|
+
openapi: string;
|
|
111
|
+
info: {
|
|
112
|
+
title: string;
|
|
113
|
+
version: string;
|
|
114
|
+
description?: string;
|
|
115
|
+
};
|
|
116
|
+
servers?: Array<{
|
|
117
|
+
url: string;
|
|
118
|
+
description?: string;
|
|
119
|
+
}>;
|
|
120
|
+
paths: Record<string, PathItem>;
|
|
121
|
+
components?: {
|
|
122
|
+
schemas?: Record<string, SchemaObject$1>;
|
|
123
|
+
parameters?: Record<string, ParameterObject$1>;
|
|
124
|
+
requestBodies?: Record<string, RequestBodyObject$1>;
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/** @typedef {import("./OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
|
|
129
|
+
/** @typedef {import("./ParsedOperation.ts").ParsedOperation} ParsedOperation */
|
|
130
|
+
/**
|
|
131
|
+
* Extract all operations from an OpenAPI spec.
|
|
132
|
+
*
|
|
133
|
+
* @param {OpenApiSpec} spec
|
|
134
|
+
* @returns {ParsedOperation[]}
|
|
135
|
+
*/
|
|
136
|
+
declare function extractOperations(spec: OpenApiSpec$a): ParsedOperation$1[];
|
|
137
|
+
type OpenApiSpec$a = OpenApiSpec$b;
|
|
138
|
+
type ParsedOperation$1 = ParsedOperation$2;
|
|
139
|
+
|
|
140
|
+
/** @typedef {import("./OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
|
|
141
|
+
/**
|
|
142
|
+
* Load an OpenAPI spec from a JSON/YAML string, URL, file path, or object.
|
|
143
|
+
*
|
|
144
|
+
* @param {string | OpenApiSpec} input
|
|
145
|
+
* @returns {Effect.Effect<OpenApiSpec, unknown>}
|
|
146
|
+
*/
|
|
147
|
+
declare function loadSpecEffect(input: string | OpenApiSpec$9): Effect.Effect<OpenApiSpec$9, unknown>;
|
|
148
|
+
type OpenApiSpec$9 = OpenApiSpec$b;
|
|
149
|
+
|
|
150
|
+
/** @typedef {import("./OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
|
|
151
|
+
/**
|
|
152
|
+
* Synchronous version for simpler call sites.
|
|
153
|
+
*
|
|
154
|
+
* @param {string | OpenApiSpec} input
|
|
155
|
+
* @returns {OpenApiSpec}
|
|
156
|
+
*/
|
|
157
|
+
declare function loadSpecSync(input: string | OpenApiSpec$8): OpenApiSpec$8;
|
|
158
|
+
type OpenApiSpec$8 = OpenApiSpec$b;
|
|
159
|
+
|
|
160
|
+
/** @typedef {import("./OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
|
|
161
|
+
/** @typedef {import("./RefObject.ts").RefObject} RefObject */
|
|
162
|
+
/** @typedef {import("./SchemaObject.ts").SchemaObject} SchemaObject */
|
|
163
|
+
/**
|
|
164
|
+
* Convert an OpenAPI JSON Schema object to a Zod schema.
|
|
165
|
+
* Falls back to z.any() for schemas that cannot be cleanly represented.
|
|
166
|
+
*
|
|
167
|
+
* @param {SchemaObject | RefObject | undefined} schema
|
|
168
|
+
* @param {OpenApiSpec} spec
|
|
169
|
+
* @param {Set<string>} [visited]
|
|
170
|
+
* @returns {z.ZodType}
|
|
171
|
+
*/
|
|
172
|
+
declare function jsonSchemaToZod(schema: SchemaObject | RefObject | undefined, spec: OpenApiSpec$7, visited?: Set<string>): z.ZodType;
|
|
173
|
+
type OpenApiSpec$7 = OpenApiSpec$b;
|
|
174
|
+
type RefObject = RefObject$1;
|
|
175
|
+
type SchemaObject = SchemaObject$1;
|
|
176
|
+
|
|
177
|
+
/** @typedef {import("./OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
|
|
178
|
+
/** @typedef {import("./ParameterObject.ts").ParameterObject} ParameterObject */
|
|
179
|
+
/** @typedef {import("./RequestBodyObject.ts").RequestBodyObject} RequestBodyObject */
|
|
180
|
+
/**
|
|
181
|
+
* Build a single Zod object schema for an operation's input, combining:
|
|
182
|
+
* - path parameters
|
|
183
|
+
* - query parameters
|
|
184
|
+
* - header parameters
|
|
185
|
+
* - request body fields
|
|
186
|
+
*
|
|
187
|
+
* @param {ParameterObject[]} parameters
|
|
188
|
+
* @param {RequestBodyObject | undefined} requestBody
|
|
189
|
+
* @param {OpenApiSpec} spec
|
|
190
|
+
* @returns {z.ZodType}
|
|
191
|
+
*/
|
|
192
|
+
declare function buildOperationSchema(parameters: ParameterObject[], requestBody: RequestBodyObject | undefined, spec: OpenApiSpec$6): z.ZodType;
|
|
193
|
+
type OpenApiSpec$6 = OpenApiSpec$b;
|
|
194
|
+
type ParameterObject = ParameterObject$1;
|
|
195
|
+
type RequestBodyObject = RequestBodyObject$1;
|
|
196
|
+
|
|
197
|
+
/** @typedef {import("../OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
|
|
198
|
+
/** @typedef {import("../OpenApiToolsOptions.ts").OpenApiToolsOptions} OpenApiToolsOptions */
|
|
199
|
+
/**
|
|
200
|
+
* Create AI SDK tools from all operations in an OpenAPI spec.
|
|
201
|
+
*
|
|
202
|
+
* @param {string | OpenApiSpec} input - OpenAPI spec as JSON object, file path, URL, or raw text
|
|
203
|
+
* @param {OpenApiToolsOptions} [options] - Configuration for auth, filtering, base URL, etc.
|
|
204
|
+
* @returns {Promise<Record<string, any>>} Record of operationId → AI SDK tool
|
|
205
|
+
*/
|
|
206
|
+
declare function createOpenApiTools(input: string | OpenApiSpec$5, options?: OpenApiToolsOptions$4): Promise<Record<string, any>>;
|
|
207
|
+
type OpenApiSpec$5 = OpenApiSpec$b;
|
|
208
|
+
type OpenApiToolsOptions$4 = OpenApiToolsOptions$5;
|
|
209
|
+
|
|
210
|
+
/** @typedef {import("../OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
|
|
211
|
+
/** @typedef {import("../OpenApiToolsOptions.ts").OpenApiToolsOptions} OpenApiToolsOptions */
|
|
212
|
+
/**
|
|
213
|
+
* Synchronous version — only works with specs that are objects or local files.
|
|
214
|
+
*
|
|
215
|
+
* @param {string | OpenApiSpec} input
|
|
216
|
+
* @param {OpenApiToolsOptions} [options]
|
|
217
|
+
* @returns {Record<string, any>}
|
|
218
|
+
*/
|
|
219
|
+
declare function createOpenApiToolsSync(input: string | OpenApiSpec$4, options?: OpenApiToolsOptions$3): Record<string, any>;
|
|
220
|
+
type OpenApiSpec$4 = OpenApiSpec$b;
|
|
221
|
+
type OpenApiToolsOptions$3 = OpenApiToolsOptions$5;
|
|
222
|
+
|
|
223
|
+
/** @typedef {import("../OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
|
|
224
|
+
/** @typedef {import("../OpenApiToolsOptions.ts").OpenApiToolsOptions} OpenApiToolsOptions */
|
|
225
|
+
/**
|
|
226
|
+
* Create a single AI SDK tool from an OpenAPI spec by operationId.
|
|
227
|
+
*
|
|
228
|
+
* @param {string | OpenApiSpec} input - OpenAPI spec as JSON object, file path, URL, or raw text
|
|
229
|
+
* @param {string} operationId - The operationId of the operation to create a tool for
|
|
230
|
+
* @param {OpenApiToolsOptions} [options] - Configuration for auth, base URL, etc.
|
|
231
|
+
* @returns {Promise<any>} A single AI SDK tool
|
|
232
|
+
*/
|
|
233
|
+
declare function createOpenApiTool(input: string | OpenApiSpec$3, operationId: string, options?: OpenApiToolsOptions$2): Promise<any>;
|
|
234
|
+
type OpenApiSpec$3 = OpenApiSpec$b;
|
|
235
|
+
type OpenApiToolsOptions$2 = OpenApiToolsOptions$5;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Type alias for an AI SDK tool produced from an OpenAPI operation.
|
|
239
|
+
* Re-exported here so JSDoc files can reference a stable name without
|
|
240
|
+
* reaching into the `ai` package directly.
|
|
241
|
+
*/
|
|
242
|
+
type OpenApiTool$1 = Tool;
|
|
243
|
+
|
|
244
|
+
/** @typedef {import("../OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
|
|
245
|
+
/** @typedef {import("../OpenApiTool.ts").OpenApiTool} OpenApiTool */
|
|
246
|
+
/** @typedef {import("../OpenApiToolsOptions.ts").OpenApiToolsOptions} OpenApiToolsOptions */
|
|
247
|
+
/**
|
|
248
|
+
* Synchronous version — only works with specs that are objects or local files.
|
|
249
|
+
*
|
|
250
|
+
* @param {string | OpenApiSpec} input
|
|
251
|
+
* @param {string} operationId
|
|
252
|
+
* @param {OpenApiToolsOptions} [options]
|
|
253
|
+
* @returns {OpenApiTool}
|
|
254
|
+
*/
|
|
255
|
+
declare function createOpenApiToolSync(input: string | OpenApiSpec$2, operationId: string, options?: OpenApiToolsOptions$1): OpenApiTool;
|
|
256
|
+
type OpenApiSpec$2 = OpenApiSpec$b;
|
|
257
|
+
type OpenApiTool = OpenApiTool$1;
|
|
258
|
+
type OpenApiToolsOptions$1 = OpenApiToolsOptions$5;
|
|
259
|
+
|
|
260
|
+
/** @typedef {import("../OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
|
|
261
|
+
/**
|
|
262
|
+
* List all operations from a spec (for CLI preview).
|
|
263
|
+
*
|
|
264
|
+
* @param {string | OpenApiSpec} input
|
|
265
|
+
* @returns {Array<{ operationId: string; method: string; path: string; summary: string }>}
|
|
266
|
+
*/
|
|
267
|
+
declare function listOperations(input: string | OpenApiSpec$1): Array<{
|
|
268
|
+
operationId: string;
|
|
269
|
+
method: string;
|
|
270
|
+
path: string;
|
|
271
|
+
summary: string;
|
|
272
|
+
}>;
|
|
273
|
+
type OpenApiSpec$1 = OpenApiSpec$b;
|
|
274
|
+
|
|
275
|
+
/** @type {import("effect").Metric.Metric.Counter<number>} */
|
|
276
|
+
declare const openApiToolCallsTotal: effect.Metric.Metric.Counter<number>;
|
|
277
|
+
/** @type {import("effect").Metric.Metric.Counter<number>} */
|
|
278
|
+
declare const openApiToolCallErrorsTotal: effect.Metric.Metric.Counter<number>;
|
|
279
|
+
/** @type {import("effect").Metric.Metric<import("effect/MetricKeyType").MetricKeyType.Histogram, number, import("effect/MetricState").MetricState.Histogram>} */
|
|
280
|
+
declare const openApiToolDuration: effect.Metric.Metric<effect_MetricKeyType.MetricKeyType.Histogram, number, effect_MetricState.MetricState.Histogram>;
|
|
281
|
+
|
|
282
|
+
type OpenApiAuth = OpenApiAuth$1;
|
|
283
|
+
type OpenApiSpec = OpenApiSpec$b;
|
|
284
|
+
type OpenApiToolsOptions = OpenApiToolsOptions$5;
|
|
285
|
+
type ParsedOperation = ParsedOperation$2;
|
|
286
|
+
|
|
287
|
+
export { type OpenApiAuth, type OpenApiSpec, type OpenApiToolsOptions, type ParsedOperation, buildOperationSchema, createOpenApiTool, createOpenApiToolSync, createOpenApiTools, createOpenApiToolsSync, extractOperations, jsonSchemaToZod, listOperations, loadSpecEffect, loadSpecSync, openApiToolCallErrorsTotal, openApiToolCallsTotal, openApiToolDuration };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @smithers-type-exports-begin
|
|
2
|
+
/** @typedef {import("./OpenApiAuth.ts").OpenApiAuth} OpenApiAuth */
|
|
3
|
+
/** @typedef {import("./OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
|
|
4
|
+
/** @typedef {import("./OpenApiToolsOptions.ts").OpenApiToolsOptions} OpenApiToolsOptions */
|
|
5
|
+
/** @typedef {import("./ParsedOperation.ts").ParsedOperation} ParsedOperation */
|
|
6
|
+
// @smithers-type-exports-end
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// OpenAPI tool factory — public API
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
export { createOpenApiTools, createOpenApiToolsSync, createOpenApiTool, createOpenApiToolSync, listOperations, } from "./tool-factory/index.js";
|
|
12
|
+
export { openApiToolCallsTotal, openApiToolCallErrorsTotal, openApiToolDuration, } from "./metrics.js";
|
|
13
|
+
export { extractOperations } from "./extractOperations.js";
|
|
14
|
+
export { loadSpecEffect } from "./loadSpecEffect.js";
|
|
15
|
+
export { loadSpecSync } from "./loadSpecSync.js";
|
|
16
|
+
export { jsonSchemaToZod } from "./jsonSchemaToZod.js";
|
|
17
|
+
export { buildOperationSchema } from "./buildOperationSchema.js";
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// OpenAPI JSON Schema → Zod schema conversion
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { isRef, resolveRef } from "./ref-resolver.js";
|
|
6
|
+
|
|
7
|
+
/** @typedef {import("./OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
|
|
8
|
+
/** @typedef {import("./RefObject.ts").RefObject} RefObject */
|
|
9
|
+
/** @typedef {import("./SchemaObject.ts").SchemaObject} SchemaObject */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Convert an OpenAPI JSON Schema object to a Zod schema.
|
|
13
|
+
* Falls back to z.any() for schemas that cannot be cleanly represented.
|
|
14
|
+
*
|
|
15
|
+
* @param {SchemaObject | RefObject | undefined} schema
|
|
16
|
+
* @param {OpenApiSpec} spec
|
|
17
|
+
* @param {Set<string>} [visited]
|
|
18
|
+
* @returns {z.ZodType}
|
|
19
|
+
*/
|
|
20
|
+
export function jsonSchemaToZod(schema, spec, visited = new Set()) {
|
|
21
|
+
if (!schema)
|
|
22
|
+
return z.any();
|
|
23
|
+
// Resolve $ref
|
|
24
|
+
if (isRef(schema)) {
|
|
25
|
+
const ref = schema.$ref;
|
|
26
|
+
if (visited.has(ref)) {
|
|
27
|
+
// Circular reference — bail to z.any()
|
|
28
|
+
return z.any().describe(`Circular reference: ${ref}`);
|
|
29
|
+
}
|
|
30
|
+
visited.add(ref);
|
|
31
|
+
const resolved = resolveRef(spec, ref);
|
|
32
|
+
const result = jsonSchemaToZod(resolved, spec, visited);
|
|
33
|
+
visited.delete(ref);
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
const s = schema;
|
|
37
|
+
// allOf — merge into a single object
|
|
38
|
+
if (s.allOf && s.allOf.length > 0) {
|
|
39
|
+
// Build a combined object from all allOf entries
|
|
40
|
+
const schemas = s.allOf.map((sub) => jsonSchemaToZod(sub, spec, visited));
|
|
41
|
+
if (schemas.length === 1)
|
|
42
|
+
return maybeDescribe(schemas[0], s);
|
|
43
|
+
// For multiple schemas, try to intersect them
|
|
44
|
+
let result = schemas[0];
|
|
45
|
+
for (let i = 1; i < schemas.length; i++) {
|
|
46
|
+
result = z.intersection(result, schemas[i]);
|
|
47
|
+
}
|
|
48
|
+
return maybeDescribe(result, s);
|
|
49
|
+
}
|
|
50
|
+
// oneOf / anyOf — union
|
|
51
|
+
if (s.oneOf && s.oneOf.length > 0) {
|
|
52
|
+
return buildUnion(s.oneOf, spec, visited, s);
|
|
53
|
+
}
|
|
54
|
+
if (s.anyOf && s.anyOf.length > 0) {
|
|
55
|
+
return buildUnion(s.anyOf, spec, visited, s);
|
|
56
|
+
}
|
|
57
|
+
const type = s.type;
|
|
58
|
+
if (type === "string") {
|
|
59
|
+
return buildString(s);
|
|
60
|
+
}
|
|
61
|
+
if (type === "number" || type === "integer") {
|
|
62
|
+
return buildNumber(s);
|
|
63
|
+
}
|
|
64
|
+
if (type === "boolean") {
|
|
65
|
+
return maybeDescribe(z.boolean(), s);
|
|
66
|
+
}
|
|
67
|
+
if (type === "array") {
|
|
68
|
+
const items = jsonSchemaToZod(s.items, spec, visited);
|
|
69
|
+
return maybeDescribe(z.array(items), s);
|
|
70
|
+
}
|
|
71
|
+
if (type === "object" || s.properties) {
|
|
72
|
+
return buildObject(s, spec, visited);
|
|
73
|
+
}
|
|
74
|
+
// null type
|
|
75
|
+
if (type === "null") {
|
|
76
|
+
return maybeDescribe(z.null(), s);
|
|
77
|
+
}
|
|
78
|
+
// Fallback
|
|
79
|
+
const desc = s.description ? `${s.description} (untyped)` : "untyped schema";
|
|
80
|
+
return z.any().describe(desc);
|
|
81
|
+
}
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Type-specific builders
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
/**
|
|
86
|
+
* @param {SchemaObject} s
|
|
87
|
+
* @returns {z.ZodType}
|
|
88
|
+
*/
|
|
89
|
+
function buildString(s) {
|
|
90
|
+
let schema;
|
|
91
|
+
if (s.enum && s.enum.length > 0) {
|
|
92
|
+
const values = s.enum;
|
|
93
|
+
schema = z.enum(values);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
let str = z.string();
|
|
97
|
+
if (s.minLength !== undefined)
|
|
98
|
+
str = str.min(s.minLength);
|
|
99
|
+
if (s.maxLength !== undefined)
|
|
100
|
+
str = str.max(s.maxLength);
|
|
101
|
+
if (s.pattern)
|
|
102
|
+
str = str.regex(new RegExp(s.pattern));
|
|
103
|
+
if (s.format === "email")
|
|
104
|
+
str = str.email();
|
|
105
|
+
if (s.format === "url" || s.format === "uri")
|
|
106
|
+
str = str.url();
|
|
107
|
+
schema = str;
|
|
108
|
+
}
|
|
109
|
+
return maybeDescribe(maybeNullable(maybeDefault(schema, s), s), s);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* @param {SchemaObject} s
|
|
113
|
+
* @returns {z.ZodType}
|
|
114
|
+
*/
|
|
115
|
+
function buildNumber(s) {
|
|
116
|
+
let num = z.number();
|
|
117
|
+
if (s.type === "integer")
|
|
118
|
+
num = num.int();
|
|
119
|
+
if (s.minimum !== undefined)
|
|
120
|
+
num = num.min(s.minimum);
|
|
121
|
+
if (s.maximum !== undefined)
|
|
122
|
+
num = num.max(s.maximum);
|
|
123
|
+
return maybeDescribe(maybeNullable(maybeDefault(num, s), s), s);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* @param {SchemaObject} s
|
|
127
|
+
* @param {OpenApiSpec} spec
|
|
128
|
+
* @param {Set<string>} visited
|
|
129
|
+
* @returns {z.ZodType}
|
|
130
|
+
*/
|
|
131
|
+
function buildObject(s, spec, visited) {
|
|
132
|
+
const props = {};
|
|
133
|
+
const required = new Set(s.required ?? []);
|
|
134
|
+
for (const [key, propSchema] of Object.entries(s.properties ?? {})) {
|
|
135
|
+
let zodProp = jsonSchemaToZod(propSchema, spec, visited);
|
|
136
|
+
if (!required.has(key)) {
|
|
137
|
+
zodProp = zodProp.optional();
|
|
138
|
+
}
|
|
139
|
+
props[key] = zodProp;
|
|
140
|
+
}
|
|
141
|
+
let obj = z.object(props);
|
|
142
|
+
if (s.additionalProperties === true || s.additionalProperties === undefined) {
|
|
143
|
+
// Allow additional properties — use catchall for objects with props
|
|
144
|
+
if (Object.keys(props).length > 0) {
|
|
145
|
+
obj = obj.catchall(z.unknown());
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return maybeDescribe(maybeNullable(obj, s), s);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* @param {Array<SchemaObject | RefObject>} variants
|
|
152
|
+
* @param {OpenApiSpec} spec
|
|
153
|
+
* @param {Set<string>} visited
|
|
154
|
+
* @param {SchemaObject} parent
|
|
155
|
+
* @returns {z.ZodType}
|
|
156
|
+
*/
|
|
157
|
+
function buildUnion(variants, spec, visited, parent) {
|
|
158
|
+
const schemas = variants.map((v) => jsonSchemaToZod(v, spec, visited));
|
|
159
|
+
if (schemas.length === 0)
|
|
160
|
+
return z.any();
|
|
161
|
+
if (schemas.length === 1)
|
|
162
|
+
return maybeDescribe(schemas[0], parent);
|
|
163
|
+
return maybeDescribe(z.union(schemas), parent);
|
|
164
|
+
}
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Helpers
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
/**
|
|
169
|
+
* @param {z.ZodType} schema
|
|
170
|
+
* @param {SchemaObject} s
|
|
171
|
+
* @returns {z.ZodType}
|
|
172
|
+
*/
|
|
173
|
+
function maybeDescribe(schema, s) {
|
|
174
|
+
if (s.description)
|
|
175
|
+
return schema.describe(s.description);
|
|
176
|
+
return schema;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* @param {z.ZodType} schema
|
|
180
|
+
* @param {SchemaObject} s
|
|
181
|
+
* @returns {z.ZodType}
|
|
182
|
+
*/
|
|
183
|
+
function maybeNullable(schema, s) {
|
|
184
|
+
if (s.nullable)
|
|
185
|
+
return schema.nullable();
|
|
186
|
+
return schema;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* @param {z.ZodType} schema
|
|
190
|
+
* @param {SchemaObject} s
|
|
191
|
+
* @returns {z.ZodType}
|
|
192
|
+
*/
|
|
193
|
+
function maybeDefault(schema, s) {
|
|
194
|
+
if (s.default !== undefined)
|
|
195
|
+
return schema.default(s.default);
|
|
196
|
+
return schema;
|
|
197
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// loadSpecEffect — Effect-based OpenAPI spec loader
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { Effect } from "effect";
|
|
6
|
+
import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
|
|
7
|
+
import { parseSpecText } from "./_specHelpers.js";
|
|
8
|
+
|
|
9
|
+
/** @typedef {import("./OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Load an OpenAPI spec from a JSON/YAML string, URL, file path, or object.
|
|
13
|
+
*
|
|
14
|
+
* @param {string | OpenApiSpec} input
|
|
15
|
+
* @returns {Effect.Effect<OpenApiSpec, unknown>}
|
|
16
|
+
*/
|
|
17
|
+
export function loadSpecEffect(input) {
|
|
18
|
+
if (typeof input === "object" && input !== null && "openapi" in input) {
|
|
19
|
+
return Effect.succeed(input);
|
|
20
|
+
}
|
|
21
|
+
const str = input;
|
|
22
|
+
// URL
|
|
23
|
+
if (str.startsWith("http://") || str.startsWith("https://")) {
|
|
24
|
+
return Effect.tryPromise({
|
|
25
|
+
try: async () => {
|
|
26
|
+
const res = await fetch(str);
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
throw new Error(`Failed to fetch OpenAPI spec: ${res.status} ${res.statusText}`);
|
|
29
|
+
}
|
|
30
|
+
const text = await res.text();
|
|
31
|
+
return parseSpecText(text);
|
|
32
|
+
},
|
|
33
|
+
catch: (cause) => toSmithersError(cause, "openapi fetch spec"),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// File path or raw JSON/YAML string
|
|
37
|
+
return Effect.try({
|
|
38
|
+
try: () => {
|
|
39
|
+
// Try reading as file first
|
|
40
|
+
try {
|
|
41
|
+
const content = readFileSync(str, "utf8");
|
|
42
|
+
return parseSpecText(content);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Not a file — try parsing as raw text
|
|
46
|
+
return parseSpecText(str);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
catch: (cause) => toSmithersError(cause, "openapi load spec"),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// loadSpecSync — synchronous OpenAPI spec loader
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { parseSpecText } from "./_specHelpers.js";
|
|
6
|
+
|
|
7
|
+
/** @typedef {import("./OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Synchronous version for simpler call sites.
|
|
11
|
+
*
|
|
12
|
+
* @param {string | OpenApiSpec} input
|
|
13
|
+
* @returns {OpenApiSpec}
|
|
14
|
+
*/
|
|
15
|
+
export function loadSpecSync(input) {
|
|
16
|
+
if (typeof input === "object" && input !== null && "openapi" in input) {
|
|
17
|
+
return input;
|
|
18
|
+
}
|
|
19
|
+
const str = input;
|
|
20
|
+
try {
|
|
21
|
+
const content = readFileSync(str, "utf8");
|
|
22
|
+
return parseSpecText(content);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return parseSpecText(str);
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/metrics.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// OpenAPI tool metrics
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
import { Metric, MetricBoundaries } from "effect";
|
|
5
|
+
|
|
6
|
+
/** @type {import("effect").Metric.Metric.Counter<number>} */
|
|
7
|
+
export const openApiToolCallsTotal = Metric.counter("smithers.openapi.tool_calls");
|
|
8
|
+
|
|
9
|
+
/** @type {import("effect").Metric.Metric.Counter<number>} */
|
|
10
|
+
export const openApiToolCallErrorsTotal = Metric.counter("smithers.openapi.tool_call_errors");
|
|
11
|
+
|
|
12
|
+
const toolBuckets = MetricBoundaries.exponential({
|
|
13
|
+
start: 10,
|
|
14
|
+
factor: 2,
|
|
15
|
+
count: 14,
|
|
16
|
+
}); // ~10ms to ~80s
|
|
17
|
+
|
|
18
|
+
/** @type {import("effect").Metric.Metric<import("effect/MetricKeyType").MetricKeyType.Histogram, number, import("effect/MetricState").MetricState.Histogram>} */
|
|
19
|
+
export const openApiToolDuration = Metric.histogram("smithers.openapi.tool_duration_ms", toolBuckets);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// $ref resolution within an OpenAPI spec
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
/** @typedef {import("./OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
|
|
5
|
+
/** @typedef {import("./RefObject.ts").RefObject} RefObject */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {unknown} obj
|
|
9
|
+
* @returns {obj is RefObject}
|
|
10
|
+
*/
|
|
11
|
+
export function isRef(obj) {
|
|
12
|
+
return (typeof obj === "object" &&
|
|
13
|
+
obj !== null &&
|
|
14
|
+
"$ref" in obj &&
|
|
15
|
+
typeof obj.$ref === "string");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a local JSON pointer ($ref) anywhere within the OpenAPI spec.
|
|
19
|
+
*
|
|
20
|
+
* @template [T=unknown]
|
|
21
|
+
* @param {OpenApiSpec} spec
|
|
22
|
+
* @param {string} ref
|
|
23
|
+
* @returns {T}
|
|
24
|
+
*/
|
|
25
|
+
export function resolveRef(spec, ref) {
|
|
26
|
+
if (!ref.startsWith("#/")) {
|
|
27
|
+
throw new Error(`Unsupported $ref format: ${ref}`);
|
|
28
|
+
}
|
|
29
|
+
const parts = ref.slice(2).split("/");
|
|
30
|
+
/** @type {unknown} */
|
|
31
|
+
let current = spec;
|
|
32
|
+
for (const part of parts) {
|
|
33
|
+
const decoded = part.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
34
|
+
if (current === null || typeof current !== "object") {
|
|
35
|
+
throw new Error(`Could not resolve $ref: ${ref}`);
|
|
36
|
+
}
|
|
37
|
+
current = /** @type {Record<string, unknown>} */ (current)[decoded];
|
|
38
|
+
if (current === undefined) {
|
|
39
|
+
throw new Error(`Could not resolve $ref: ${ref}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return /** @type {T} */ (current);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* If the value is a $ref, resolve it. Otherwise return as-is.
|
|
46
|
+
* Handles one level of indirection (resolved value is not recursively resolved).
|
|
47
|
+
*
|
|
48
|
+
* @template [T=unknown]
|
|
49
|
+
* @param {OpenApiSpec} spec
|
|
50
|
+
* @param {T | RefObject} value
|
|
51
|
+
* @returns {T}
|
|
52
|
+
*/
|
|
53
|
+
export function deref(spec, value) {
|
|
54
|
+
if (isRef(value)) {
|
|
55
|
+
return resolveRef(spec, value.$ref);
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|