/**
* @module pipeline/openApiParser
* @description Parses OpenAPI 3.x / Swagger 2.x JSON specs into ApiEndpoint[]
* descriptors compatible with buildApiTestPrompt.
*
* Supports JSON format only (covers the vast majority of specs). YAML support
* can be added later with a lightweight parser.
*
* ### Exports
* - {@link parseOpenApiSpec} — `(specText) → ApiEndpoint[]`
*/
/**
* Attempt to generate an example value from a JSON Schema property.
* @param {object} schema
* @returns {*}
*/
function exampleFromSchema(schema) {
if (!schema || typeof schema !== "object") return undefined;
if (schema.example !== undefined) return schema.example;
if (schema.default !== undefined) return schema.default;
if (schema.enum?.length) return schema.enum[0];
switch (schema.type) {
case "string": return schema.format === "email" ? "user@example.com" : "string";
case "integer": return schema.minimum ?? 1;
case "number": return schema.minimum ?? 1.0;
case "boolean": return true;
case "array":
if (schema.items) return [exampleFromSchema(schema.items)];
return [];
case "object": {
const obj = {};
for (const [key, prop] of Object.entries(schema.properties || {})) {
obj[key] = exampleFromSchema(prop);
}
return obj;
}
default: return undefined;
}
}
/**
* Resolve a $ref path like "#/components/schemas/User" against the root spec.
* Only supports local JSON pointer refs (the most common kind).
* @param {string} ref
* @param {object} root — the full spec object
* @returns {object|null}
*/
function resolveRef(ref, root) {
if (!ref || typeof ref !== "string" || !ref.startsWith("#/")) return null;
const parts = ref.slice(2).split("/");
let current = root;
for (const part of parts) {
if (!current || typeof current !== "object") return null;
current = current[part];
}
return current || null;
}
/**
* Recursively resolve $ref in a schema object (one level deep to avoid loops).
* @param {object} schema
* @param {object} root
* @returns {object}
*/
function resolveSchema(schema, root) {
if (!schema) return schema;
if (schema.$ref) return resolveRef(schema.$ref, root) || schema;
if (schema.properties) {
const resolved = { ...schema, properties: {} };
for (const [key, prop] of Object.entries(schema.properties)) {
resolved.properties[key] = prop.$ref ? (resolveRef(prop.$ref, root) || prop) : prop;
}
return resolved;
}
return schema;
}
/**
* Extract request body example from an OpenAPI 3.x operation.
* @param {object} operation — the operation object (get/post/put etc.)
* @param {object} root — full spec for $ref resolution
* @returns {string|null}
*/
function extractRequestBody(operation, root) {
const rb = operation.requestBody;
if (!rb) return null;
const resolved = rb.$ref ? resolveRef(rb.$ref, root) : rb;
const jsonContent = resolved?.content?.["application/json"];
if (!jsonContent) return null;
if (jsonContent.example) return JSON.stringify(jsonContent.example);
if (jsonContent.schema) {
const schema = resolveSchema(jsonContent.schema, root);
const example = exampleFromSchema(schema);
if (example !== undefined) return JSON.stringify(example);
}
return null;
}
/**
* Extract response body example from an OpenAPI 3.x operation.
* Tries the success response (200/201) first.
* @param {object} operation
* @param {object} root
* @returns {string|null}
*/
function extractResponseBody(operation, root) {
const responses = operation.responses;
if (!responses) return null;
// Try success codes first, then any code
for (const code of ["200", "201", "default"]) {
const resp = responses[code];
if (!resp) continue;
const resolved = resp.$ref ? resolveRef(resp.$ref, root) : resp;
const jsonContent = resolved?.content?.["application/json"];
if (!jsonContent) continue;
if (jsonContent.example) return JSON.stringify(jsonContent.example);
if (jsonContent.schema) {
const schema = resolveSchema(jsonContent.schema, root);
const example = exampleFromSchema(schema);
if (example !== undefined) return JSON.stringify(example);
}
}
return null;
}
/**
* Parse an OpenAPI 3.x or Swagger 2.x JSON spec into ApiEndpoint[] descriptors.
*
* @param {string} specText — raw JSON string of the OpenAPI spec
* @returns {ApiEndpoint[]} — endpoint descriptors, or empty array if parsing fails
*/
export function parseOpenApiSpec(specText) {
let spec;
try {
spec = JSON.parse(specText);
} catch {
return []; // not valid JSON — caller should fall back to text-based parsing
}
// Must have paths object (both OpenAPI 3.x and Swagger 2.x)
if (!spec.paths || typeof spec.paths !== "object") return [];
// Detect if this is actually an OpenAPI spec (not random JSON)
const isOpenApi = spec.openapi || spec.swagger;
if (!isOpenApi) return [];
const endpoints = [];
const validMethods = new Set(["get", "post", "put", "patch", "delete"]);
for (const [pathPattern, pathItem] of Object.entries(spec.paths)) {
if (!pathItem || typeof pathItem !== "object") continue;
for (const [method, operation] of Object.entries(pathItem)) {
if (!validMethods.has(method.toLowerCase())) continue;
if (!operation || typeof operation !== "object") continue;
const upperMethod = method.toUpperCase();
// Extract status codes from responses
const statuses = operation.responses
? Object.keys(operation.responses).filter(c => /^\d+$/.test(c)).map(Number).sort()
: upperMethod === "GET" ? [200] : [200, 201];
endpoints.push({
method: upperMethod,
pathPattern,
exampleUrls: [pathPattern],
statuses: statuses.length > 0 ? statuses : [200],
contentType: "application/json",
requestBodyExample: extractRequestBody(operation, spec),
responseBodyExample: extractResponseBody(operation, spec),
callCount: 1,
avgDurationMs: 0,
pageUrls: [],
});
}
}
return endpoints;
}