UNPKG

@mintlify/validation

Version:

Validates mint.json files

295 lines (294 loc) 13.8 kB
/** * Converts a SchemaGraphData back to an OpenAPI specification. * Optionally filters to a specific operation if path and method are provided. * * This is the inverse of openApiToSchemaGraph - it reconstructs the original * OpenAPI document from the graph representation. */ export function schemaGraphToOpenApi({ schemaGraphData, targetPath, targetMethod, }) { const { spec, documentId } = schemaGraphData; const { uuidObjectHashMap, hashedNodeMap, refUuidMap } = spec; // Build reverse map: UUID -> $ref path const uuidRefMap = new Map(); for (const [refPath, uuid] of Object.entries(refUuidMap)) { uuidRefMap.set(uuid, refPath); } // Track which component refs are actually used const usedComponents = new Set(); // Helper to resolve a node by UUID const resolveNode = (uuid) => { const hash = uuidObjectHashMap[uuid]; if (!hash) return undefined; // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return hashedNodeMap[hash]; }; // Track which security schemes are used const usedSecuritySchemes = new Set(); // Helper to transform security requirements from internal format to OpenAPI format const transformSecurityRequirement = (securityReq) => { const transformed = {}; for (const [key, value] of Object.entries(securityReq)) { // Track this security scheme as used usedSecuritySchemes.add(key); if (typeof value === 'object' && !Array.isArray(value) && 'scopes' in value) { // Extract just the scopes array from internal format // Create a new array to avoid YAML anchor references transformed[key] = value.scopes ? [...value.scopes] : []; } else if (Array.isArray(value)) { // Already in correct format - create a new array to avoid shared references transformed[key] = [...value]; } } return transformed; }; // Helper to convert UUID references back to $ref or inline objects const convertValue = (value, depth = 0) => { // Prevent infinite recursion - return empty object as fallback if (depth > 50) return {}; if (value == null) return value; // If it's a UUID string, try to resolve it if (typeof value === 'string') { const refPath = uuidRefMap.get(value); // If this UUID corresponds to a component ref, use $ref if (refPath && refPath.startsWith('#/components/')) { usedComponents.add(refPath); return { $ref: refPath }; } // Otherwise try to resolve and inline it const resolved = resolveNode(value); if (resolved && typeof resolved === 'object') { return convertValue(resolved, depth + 1); } return value; } // Handle arrays if (Array.isArray(value)) { return value.map((item) => convertValue(item, depth + 1)); } // Handle objects if (typeof value === 'object') { // Special handling for $ref objects (OpenAPI 3.1 allows sibling properties) if ('$ref' in value) { const refValue = value.$ref; // Convert the $ref value (which may be a UUID) to the actual ref path let convertedRef; if (typeof refValue === 'string') { const refPath = uuidRefMap.get(refValue); if (refPath && refPath.startsWith('#/components/')) { usedComponents.add(refPath); convertedRef = refPath; } else { convertedRef = refValue; } } else { convertedRef = convertValue(refValue, depth + 1); } // If there are only $ref property, return just the $ref object if (Object.keys(value).length === 1) { return { $ref: convertedRef }; } // OpenAPI 3.1 allows sibling properties with $ref // Process all properties, with special handling for $ref const result = { $ref: convertedRef }; for (const [key, val] of Object.entries(value)) { if (key !== '$ref') { result[key] = convertValue(val, depth + 1); } } return result; } const result = {}; for (const [key, val] of Object.entries(value)) { result[key] = convertValue(val, depth + 1); } return result; } return value; }; // Get the document root const docObj = resolveNode(documentId); if (!docObj) { throw new Error('Could not resolve document root'); } // Start building the OpenAPI document const openApiDoc = { openapi: '3.0.0', // Default, will be overwritten if present in document info: { title: 'API Documentation', version: '1.0.0', }, }; // Copy over document-level properties if (docObj.openapi) openApiDoc.openapi = docObj.openapi; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (docObj.info) { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions openApiDoc.info = convertValue(docObj.info); } // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (docObj.servers) { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions openApiDoc.servers = convertValue(docObj.servers); } // Security requirements need special handling - they should be resolved inline, not as $refs // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (docObj.security && Array.isArray(docObj.security)) { const securityArray = []; for (const securityUuid of docObj.security) { if (typeof securityUuid === 'string') { const securityReq = resolveNode(securityUuid); if (securityReq) { securityArray.push(transformSecurityRequirement(securityReq)); } } } // Always set security, even if empty - security: [] has specific meaning (public endpoint) openApiDoc.security = securityArray; } if (docObj.tags) { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions openApiDoc.tags = convertValue(docObj.tags); } if (docObj.externalDocs) { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions openApiDoc.externalDocs = convertValue(docObj.externalDocs); } // Build paths openApiDoc.paths = {}; const pathUuids = docObj.paths; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (pathUuids && Array.isArray(pathUuids)) { for (const pathUuid of pathUuids) { const pathObj = resolveNode(pathUuid); if (!pathObj || !('path' in pathObj)) continue; const pathKey = pathObj.path; // If filtering by path, skip non-matching paths if (targetPath && pathKey !== targetPath) continue; const pathItem = {}; // Copy path-level properties (with type guard) if ('summary' in pathObj && pathObj.summary) pathItem.summary = pathObj.summary; if ('description' in pathObj && pathObj.description) pathItem.description = pathObj.description; if ('servers' in pathObj && pathObj.servers) pathItem.servers = convertValue(pathObj.servers); if ('parameters' in pathObj && pathObj.parameters) pathItem.parameters = convertValue(pathObj.parameters); // Process operations - they're stored directly on the path object as method properties const httpMethods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']; // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const pathObjRecord = pathObj; for (const method of httpMethods) { const operationUuid = pathObjRecord[method]; if (!operationUuid || typeof operationUuid !== 'string') continue; // If filtering by method, skip non-matching methods if (targetMethod && method.toLowerCase() !== targetMethod.toLowerCase()) continue; const operation = resolveNode(operationUuid); if (!operation) continue; const operationObj = {}; // Copy operation properties if (operation.tags) operationObj.tags = convertValue(operation.tags); if (operation.summary) operationObj.summary = operation.summary; if (operation.description) operationObj.description = operation.description; if (operation.operationId) operationObj.operationId = operation.operationId; if (operation.parameters) operationObj.parameters = convertValue(operation.parameters); if (operation.requestBody) operationObj.requestBody = convertValue(operation.requestBody); if (operation.responses) operationObj.responses = convertValue(operation.responses); if (operation.callbacks) operationObj.callbacks = convertValue(operation.callbacks); if (operation.deprecated !== undefined) operationObj.deprecated = operation.deprecated; // Security requirements need special handling at operation level too if (operation.security && Array.isArray(operation.security)) { const securityArray = []; for (const securityUuid of operation.security) { if (typeof securityUuid === 'string') { const securityReq = resolveNode(securityUuid); if (securityReq) { securityArray.push(transformSecurityRequirement(securityReq)); } } } // Always set security, even if empty - security: [] has specific meaning (public endpoint) operationObj.security = securityArray; } if (operation.servers) operationObj.servers = convertValue(operation.servers); if (operation.externalDocs) operationObj.externalDocs = convertValue(operation.externalDocs); pathItem[method] = operationObj; } // Only add the path if it has content (at least one operation) const hasOperations = Object.keys(pathItem).some((key) => ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'].includes(key)); if (hasOperations) { openApiDoc.paths[pathKey] = pathItem; } } } // Build components section with only used components and security schemes if (usedComponents.size > 0 || usedSecuritySchemes.size > 0) { openApiDoc.components = {}; // Cast to allow dynamic indexing // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const components = openApiDoc.components; // Add used schema/parameter/response components (referenced via $ref) for (const refPath of usedComponents) { // Parse the ref path: #/components/{type}/{name} const match = refPath.match(/^#\/components\/([^/]+)\/(.+)$/); if (!match) continue; const [, componentType, componentName] = match; if (!componentType || !componentName) continue; const uuid = refUuidMap[refPath]; if (!uuid) continue; const component = resolveNode(uuid); if (!component) continue; // Initialize the component type object if it doesn't exist if (!components[componentType]) { components[componentType] = {}; } // Add the component (recursively converting any nested UUIDs) components[componentType][componentName] = convertValue(component); } // Add used security schemes (referenced by name in security requirements) for (const schemeName of usedSecuritySchemes) { const refPath = `#/components/securitySchemes/${schemeName}`; const uuid = refUuidMap[refPath]; if (!uuid) continue; const scheme = resolveNode(uuid); if (!scheme) continue; // Initialize security schemes object if it doesn't exist if (!components.securitySchemes) { components.securitySchemes = {}; } // Add the security scheme components.securitySchemes[schemeName] = convertValue(scheme); } } // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return openApiDoc; }