@spec2tools/core 0.1.1 → 0.1.2

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.
@@ -109,10 +109,39 @@ function parseSecurityScheme(scheme, scopes) {
109
109
  }
110
110
  return { type: 'none' };
111
111
  }
112
+ /**
113
+ * Resolve a $ref to its actual schema object
114
+ */
115
+ function resolveRef(ref, spec, visited = new Set()) {
116
+ // Detect circular references
117
+ if (visited.has(ref)) {
118
+ throw new UnsupportedSchemaError(ref, 'Circular $ref detected');
119
+ }
120
+ visited.add(ref);
121
+ // Only support #/components/schemas/ references
122
+ if (!ref.startsWith('#/components/schemas/')) {
123
+ throw new UnsupportedSchemaError(ref, 'Only #/components/schemas/ $refs are supported');
124
+ }
125
+ const schemaName = ref.replace('#/components/schemas/', '');
126
+ const schema = spec.components?.schemas?.[schemaName];
127
+ if (!schema) {
128
+ throw new UnsupportedSchemaError(ref, `Schema "${schemaName}" not found in components`);
129
+ }
130
+ // If the resolved schema has a $ref, resolve it recursively
131
+ if (schema.$ref) {
132
+ return resolveRef(schema.$ref, spec, visited);
133
+ }
134
+ return schema;
135
+ }
112
136
  /**
113
137
  * Convert an OpenAPI schema to a Zod schema
114
138
  */
115
- function schemaToZod(schema, path, depth = 0) {
139
+ function schemaToZod(schema, path, spec, depth = 0, visited = new Set()) {
140
+ // Resolve $ref if present
141
+ if (schema.$ref) {
142
+ const resolvedSchema = resolveRef(schema.$ref, spec, new Set(visited));
143
+ return schemaToZod(resolvedSchema, path, spec, depth, visited);
144
+ }
116
145
  // Check for unsupported features
117
146
  if (schema.anyOf) {
118
147
  throw new UnsupportedSchemaError(path, 'anyOf is not supported');
@@ -123,9 +152,6 @@ function schemaToZod(schema, path, depth = 0) {
123
152
  if (schema.allOf) {
124
153
  throw new UnsupportedSchemaError(path, 'allOf is not supported');
125
154
  }
126
- if (schema.$ref) {
127
- throw new UnsupportedSchemaError(path, '$ref is not supported');
128
- }
129
155
  // Handle array types
130
156
  if (schema.type === 'array') {
131
157
  if (!schema.items) {
@@ -134,7 +160,7 @@ function schemaToZod(schema, path, depth = 0) {
134
160
  if (schema.items.type === 'object') {
135
161
  throw new UnsupportedSchemaError(path, 'Arrays of objects are not supported');
136
162
  }
137
- const itemSchema = schemaToZod(schema.items, `${path}.items`, depth);
163
+ const itemSchema = schemaToZod(schema.items, `${path}.items`, spec, depth, visited);
138
164
  let arraySchema = z.array(itemSchema);
139
165
  if (schema.description) {
140
166
  arraySchema = arraySchema.describe(schema.description);
@@ -150,7 +176,7 @@ function schemaToZod(schema, path, depth = 0) {
150
176
  const required = schema.required || [];
151
177
  const shape = {};
152
178
  for (const [propName, propSchema] of Object.entries(properties)) {
153
- let zodProp = schemaToZod(propSchema, `${path}.${propName}`, depth + 1);
179
+ let zodProp = schemaToZod(propSchema, `${path}.${propName}`, spec, depth + 1, visited);
154
180
  if (!required.includes(propName)) {
155
181
  zodProp = zodProp.optional();
156
182
  }
@@ -197,15 +223,17 @@ function schemaToZod(schema, path, depth = 0) {
197
223
  /**
198
224
  * Build parameters schema from path and query parameters
199
225
  */
200
- function buildParametersSchema(parameters, operationId) {
226
+ function buildParametersSchema(parameters, operationId, spec) {
201
227
  const shape = {};
228
+ const pathParams = new Set();
229
+ const queryParams = new Set();
202
230
  for (const param of parameters) {
203
231
  if (param.in !== 'path' && param.in !== 'query') {
204
232
  continue; // Skip header and cookie parameters
205
233
  }
206
234
  let paramSchema;
207
235
  if (param.schema) {
208
- paramSchema = schemaToZod(param.schema, `${operationId}.parameters.${param.name}`, 0);
236
+ paramSchema = schemaToZod(param.schema, `${operationId}.parameters.${param.name}`, spec, 0);
209
237
  }
210
238
  else {
211
239
  paramSchema = z.string();
@@ -217,45 +245,58 @@ function buildParametersSchema(parameters, operationId) {
217
245
  paramSchema = paramSchema.optional();
218
246
  }
219
247
  shape[param.name] = paramSchema;
248
+ // Track which set this parameter belongs to
249
+ if (param.in === 'path') {
250
+ pathParams.add(param.name);
251
+ }
252
+ else if (param.in === 'query') {
253
+ queryParams.add(param.name);
254
+ }
220
255
  }
221
- return shape;
256
+ return { shape, pathParams, queryParams };
222
257
  }
223
258
  /**
224
259
  * Build request body schema
225
260
  */
226
- function buildRequestBodySchema(operation, operationId) {
261
+ function buildRequestBodySchema(operation, operationId, spec) {
262
+ const shape = {};
263
+ const bodyParams = new Set();
227
264
  if (!operation.requestBody?.content) {
228
- return {};
265
+ return { shape, bodyParams };
229
266
  }
230
267
  const jsonContent = operation.requestBody.content['application/json'];
231
268
  if (!jsonContent?.schema) {
232
- return {};
269
+ return { shape, bodyParams };
233
270
  }
234
271
  const schema = jsonContent.schema;
235
272
  // Check for file uploads
236
273
  if (schema.type === 'string' && schema.format === 'binary') {
237
274
  throw new UnsupportedSchemaError(`${operationId}.requestBody`, 'File uploads are not supported');
238
275
  }
276
+ // Resolve $ref if present
277
+ let resolvedSchema = schema;
278
+ if (schema.$ref) {
279
+ resolvedSchema = resolveRef(schema.$ref, spec);
280
+ }
239
281
  // For object schemas, flatten properties into the parameter shape
240
- if (schema.type === 'object' || schema.properties) {
241
- const properties = schema.properties || {};
242
- const required = schema.required || [];
243
- const shape = {};
282
+ if (resolvedSchema.type === 'object' || resolvedSchema.properties) {
283
+ const properties = resolvedSchema.properties || {};
284
+ const required = resolvedSchema.required || [];
244
285
  for (const [propName, propSchema] of Object.entries(properties)) {
245
286
  // Check for file upload in properties
246
287
  if (propSchema.type === 'string' &&
247
288
  propSchema.format === 'binary') {
248
289
  throw new UnsupportedSchemaError(`${operationId}.requestBody.${propName}`, 'File uploads are not supported');
249
290
  }
250
- let zodProp = schemaToZod(propSchema, `${operationId}.requestBody.${propName}`, 1);
291
+ let zodProp = schemaToZod(propSchema, `${operationId}.requestBody.${propName}`, spec, 1);
251
292
  if (!required.includes(propName)) {
252
293
  zodProp = zodProp.optional();
253
294
  }
254
295
  shape[propName] = zodProp;
296
+ bodyParams.add(propName); // Track that this is a body parameter
255
297
  }
256
- return shape;
257
298
  }
258
- return {};
299
+ return { shape, bodyParams };
259
300
  }
260
301
  /**
261
302
  * Generate tool name from operation
@@ -292,9 +333,9 @@ export function parseOperations(spec) {
292
333
  ...(operation.parameters || []),
293
334
  ];
294
335
  // Build combined schema from parameters and request body
295
- const parameterShape = buildParametersSchema(allParameters, operationId);
296
- const bodyShape = buildRequestBodySchema(operation, operationId);
297
- const combinedShape = { ...parameterShape, ...bodyShape };
336
+ const parametersResult = buildParametersSchema(allParameters, operationId, spec);
337
+ const bodyResult = buildRequestBodySchema(operation, operationId, spec);
338
+ const combinedShape = { ...parametersResult.shape, ...bodyResult.shape };
298
339
  const parameters = z.object(combinedShape);
299
340
  // Extract operation-specific auth config
300
341
  const authConfig = extractOperationAuthConfig(spec, operation);
@@ -305,6 +346,11 @@ export function parseOperations(spec) {
305
346
  httpMethod: method,
306
347
  path,
307
348
  authConfig,
349
+ parameterMetadata: {
350
+ pathParams: parametersResult.pathParams,
351
+ queryParams: parametersResult.queryParams,
352
+ bodyParams: bodyResult.bodyParams,
353
+ },
308
354
  });
309
355
  }
310
356
  }
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { Tool, HttpMethod, AuthConfig } from './types.js';
2
+ import { Tool, HttpMethod, AuthConfig, ParameterMetadata } from './types.js';
3
3
  import { AuthManager } from './auth-manager.js';
4
4
  interface ToolDefinition {
5
5
  name: string;
@@ -8,6 +8,7 @@ interface ToolDefinition {
8
8
  httpMethod: HttpMethod;
9
9
  path: string;
10
10
  authConfig?: AuthConfig;
11
+ parameterMetadata?: ParameterMetadata;
11
12
  }
12
13
  /**
13
14
  * Create executable tools from tool definitions
@@ -19,8 +19,8 @@ function createExecutor(tool, baseUrl, authManager) {
19
19
  const validatedParams = tool.parameters.parse(params);
20
20
  // Build URL with path parameters replaced
21
21
  let url = buildUrl(baseUrl, tool.path, validatedParams);
22
- // Separate path, query, and body parameters
23
- const { queryParams, bodyParams } = separateParams(validatedParams, tool.path);
22
+ // Separate path, query, and body parameters using metadata
23
+ const { queryParams, bodyParams } = separateParams(validatedParams, tool.parameterMetadata);
24
24
  // Add query parameters
25
25
  const urlObj = new URL(url);
26
26
  for (const [key, value] of Object.entries(queryParams)) {
@@ -122,39 +122,55 @@ function buildUrl(baseUrl, path, params) {
122
122
  return `${baseUrl}${finalPath}`;
123
123
  }
124
124
  /**
125
- * Separate parameters into path, query, and body params
125
+ * Separate parameters into path, query, and body params using metadata
126
126
  */
127
- function separateParams(params, path) {
127
+ function separateParams(params, metadata) {
128
128
  const pathParams = {};
129
129
  const queryParams = {};
130
130
  const bodyParams = {};
131
- // Extract path parameter names
132
- const pathParamNames = new Set();
133
- const pathParamRegex = /\{(\w+)\}/g;
134
- let match;
135
- while ((match = pathParamRegex.exec(path)) !== null) {
136
- pathParamNames.add(match[1]);
137
- }
138
- for (const [key, value] of Object.entries(params)) {
139
- if (pathParamNames.has(key)) {
140
- pathParams[key] = value;
141
- }
142
- else if (isPrimitive(value)) {
143
- // Primitive values go to query params
144
- queryParams[key] = value;
131
+ // If we have metadata, use it to categorize parameters
132
+ if (metadata) {
133
+ for (const [key, value] of Object.entries(params)) {
134
+ if (metadata.pathParams.has(key)) {
135
+ pathParams[key] = value;
136
+ }
137
+ else if (metadata.queryParams.has(key)) {
138
+ queryParams[key] = value;
139
+ }
140
+ else if (metadata.bodyParams.has(key)) {
141
+ bodyParams[key] = value;
142
+ }
143
+ else {
144
+ // Fallback: if not in metadata, treat as body param
145
+ bodyParams[key] = value;
146
+ }
145
147
  }
146
- else {
147
- // Complex values go to body
148
- bodyParams[key] = value;
148
+ }
149
+ else {
150
+ // Fallback to old behavior if no metadata (shouldn't happen with new parser)
151
+ // This keeps backward compatibility
152
+ const pathParamNames = extractPathParamNames(params);
153
+ for (const [key, value] of Object.entries(params)) {
154
+ if (pathParamNames.has(key)) {
155
+ pathParams[key] = value;
156
+ }
157
+ else if (isPrimitive(value)) {
158
+ queryParams[key] = value;
159
+ }
160
+ else {
161
+ bodyParams[key] = value;
162
+ }
149
163
  }
150
164
  }
151
- // For simplicity, if there are non-path primitive params,
152
- // we need to determine if they're query or body based on HTTP method
153
- // Since we don't have that info here, we'll treat all non-path primitives
154
- // as potential query params for GET/DELETE, and body params for POST/PUT/PATCH
155
- // This is handled by the executor which knows the method
156
165
  return { pathParams, queryParams, bodyParams };
157
166
  }
167
+ /**
168
+ * Extract path parameter names from params (fallback)
169
+ */
170
+ function extractPathParamNames(params) {
171
+ // This is a fallback - in practice, metadata should always be provided
172
+ return new Set();
173
+ }
158
174
  /**
159
175
  * Check if value is a primitive type
160
176
  */
package/dist/types.d.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  import { z } from 'zod';
2
2
  export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
3
+ export interface ParameterMetadata {
4
+ /** Parameters that come from path (e.g., {id} in /users/{id}) */
5
+ pathParams: Set<string>;
6
+ /** Parameters that come from query string */
7
+ queryParams: Set<string>;
8
+ /** Parameters that come from request body */
9
+ bodyParams: Set<string>;
10
+ }
3
11
  export interface Tool {
4
12
  name: string;
5
13
  description: string;
@@ -8,6 +16,7 @@ export interface Tool {
8
16
  httpMethod: HttpMethod;
9
17
  path: string;
10
18
  authConfig?: AuthConfig;
19
+ parameterMetadata?: ParameterMetadata;
11
20
  }
12
21
  export type AuthType = 'oauth2' | 'apiKey' | 'bearer' | 'basic' | 'none';
13
22
  export interface AuthConfig {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spec2tools/core",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Core utilities for OpenAPI parsing and authentication",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",