@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/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
+ }
@@ -0,0 +1,2 @@
1
+ export { jsonSchemaToZod } from "./jsonSchemaToZod.js";
2
+ export { buildOperationSchema } from "./buildOperationSchema.js";
@@ -0,0 +1,3 @@
1
+ export { loadSpecEffect } from "./loadSpecEffect.js";
2
+ export { loadSpecSync } from "./loadSpecSync.js";
3
+ export { extractOperations } from "./extractOperations.js";