feat(common): lenient OpenAPI imports (#4917)

Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
Anwarul Islam 2025-04-09 20:14:35 +06:00 committed by GitHub
parent 8a1996126d
commit 965c722ff8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -55,6 +55,29 @@ const objectHasProperty = <T extends string>(
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<Record<string, unknown>>
| OpenAPIV3.PathItemObject<Record<string, unknown>>
@ -746,8 +753,8 @@ const parseOpenAPIUrl = (
**/
if (objectHasProperty(doc, "swagger")) {
const { host = "<<baseUrl>>", basePath = "" } =
doc satisfies OpenAPIV2.Document
const host = doc.host?.trim() || "<<baseUrl>>"
const basePath = doc.basePath?.trim() || ""
return `${host}${basePath}`
}
@ -760,7 +767,7 @@ const parseOpenAPIUrl = (
return doc.servers?.[0]?.url ?? "<<baseUrl>>"
}
// 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 "<<baseUrl>>"
}
@ -853,7 +860,17 @@ const convertPathToHoppReqs = (
const convertOpenApiDocsToHopp = (
docs: OpenAPI.Document[]
): TE.TaskEither<never, HoppCollection[]> => {
): TE.TaskEither<string, HoppCollection[]> => {
// 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
}
)
)
),