feat(common): lenient OpenAPI imports (#4917)
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
parent
8a1996126d
commit
965c722ff8
1 changed files with 73 additions and 29 deletions
|
|
@ -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
|
||||
}
|
||||
)
|
||||
)
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in a new issue