From 965c722ff8749d06cabba6de49ce5bf436607825 Mon Sep 17 00:00:00 2001 From: Anwarul Islam Date: Wed, 9 Apr 2025 20:14:35 +0600 Subject: [PATCH] feat(common): lenient OpenAPI imports (#4917) Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com> --- .../helpers/import-export/import/openapi.ts | 102 +++++++++++++----- 1 file changed, 73 insertions(+), 29 deletions(-) diff --git a/packages/hoppscotch-common/src/helpers/import-export/import/openapi.ts b/packages/hoppscotch-common/src/helpers/import-export/import/openapi.ts index 2bdffdcf..20757f6c 100644 --- a/packages/hoppscotch-common/src/helpers/import-export/import/openapi.ts +++ b/packages/hoppscotch-common/src/helpers/import-export/import/openapi.ts @@ -55,6 +55,29 @@ const objectHasProperty = ( typeof obj === "object" && Object.prototype.hasOwnProperty.call(obj, propName) +// Helper function to check for unresolved references in a document +const hasUnresolvedRefs = (obj: unknown, visited = new WeakSet()): boolean => { + // Handle non-objects or null + if (!obj || typeof obj !== "object") return false + + // Check for circular references + if (visited.has(obj)) return false + + // Add current object to visited set + visited.add(obj) + + // Check if current object has $ref property + if ("$ref" in obj && typeof obj.$ref === "string") return true + + // Check arrays + if (Array.isArray(obj)) { + return obj.some((item) => hasUnresolvedRefs(item, visited)) + } + + // Check object properties + return Object.values(obj).some((value) => hasUnresolvedRefs(value, visited)) +} + // basic validation for OpenAPI V2 Document const isOpenAPIV2Document = (doc: unknown): doc is OpenAPIV2.Document => { return ( @@ -75,22 +98,6 @@ const isOpenAPIV3Document = ( ) } -const hasRequiredOpenAPIFields = (doc: unknown): boolean => { - return ( - objectHasProperty(doc, "info") && - objectHasProperty(doc.info, "title") && - objectHasProperty(doc.info, "version") && - objectHasProperty(doc, "paths") - ) -} - -const isABasicOpenAPIDoc = (doc: unknown): boolean => { - return ( - (isOpenAPIV2Document(doc) || isOpenAPIV3Document(doc)) && - hasRequiredOpenAPIFields(doc) - ) -} - type OpenAPIPathInfoType = | OpenAPIV2.PathItemObject> | OpenAPIV3.PathItemObject> @@ -746,8 +753,8 @@ const parseOpenAPIUrl = ( **/ if (objectHasProperty(doc, "swagger")) { - const { host = "<>", basePath = "" } = - doc satisfies OpenAPIV2.Document + const host = doc.host?.trim() || "<>" + const basePath = doc.basePath?.trim() || "" return `${host}${basePath}` } @@ -760,7 +767,7 @@ const parseOpenAPIUrl = ( return doc.servers?.[0]?.url ?? "<>" } - // If the document is neither v2 nor v3 then return a env variable as placeholder + // If the document is neither v2 nor v3 or missing required fields return "<>" } @@ -853,7 +860,17 @@ const convertPathToHoppReqs = ( const convertOpenApiDocsToHopp = ( docs: OpenAPI.Document[] -): TE.TaskEither => { +): TE.TaskEither => { + // checking for unresolved references before conversion + for (const doc of docs) { + if (hasUnresolvedRefs(doc)) { + console.warn( + "Document contains unresolved references which may affect import quality" + ) + // continue anyway to provide a best-effort import + } + } + const collections = docs.map((doc) => { const name = doc.info.title @@ -930,15 +947,28 @@ export const hoppOpenAPIImporter = (fileContents: string[]) => for (const docObj of docArr) { try { - const isValidOpenAPISpec = isABasicOpenAPIDoc(docObj) + // More lenient check - if it has paths, we'll try to import it + const isValidOpenAPISpec = + objectHasProperty(docObj, "paths") && + (isOpenAPIV2Document(docObj) || + isOpenAPIV3Document(docObj) || + objectHasProperty(docObj, "info")) if (!isValidOpenAPISpec) { throw new Error("INVALID_OPENAPI_SPEC") } - const validatedDoc = await validateDocs(docObj) - - resultDoc.push(validatedDoc) + try { + const validatedDoc = await validateDocs(docObj) + resultDoc.push(validatedDoc) + } catch (validationError) { + // If validation fails but it has basic OpenAPI structure, add it anyway + if (objectHasProperty(docObj, "paths")) { + resultDoc.push(docObj as OpenAPI.Document) + } else { + throw validationError + } + } } catch (err) { if ( err instanceof Error && @@ -960,10 +990,11 @@ export const hoppOpenAPIImporter = (fileContents: string[]) => } } } - return resultDoc }, - () => IMPORTER_INVALID_FILE_FORMAT + () => { + return IMPORTER_INVALID_FILE_FORMAT + } ) ) }), @@ -975,14 +1006,27 @@ export const hoppOpenAPIImporter = (fileContents: string[]) => const resultDoc = [] for (const docObj of docArr) { - const validatedDoc = await dereferenceDocs(docObj) + try { + const validatedDoc = await dereferenceDocs(docObj) + resultDoc.push(validatedDoc) + } catch (error) { + // Check if the document has unresolved references + if (hasUnresolvedRefs(docObj)) { + console.warn( + "Document contains unresolved references which may affect import quality" + ) + } - resultDoc.push(validatedDoc) + // If dereferencing fails, use the original document + resultDoc.push(docObj) + } } return resultDoc }, - () => OPENAPI_DEREF_ERROR + () => { + return OPENAPI_DEREF_ERROR + } ) ) ),