feat: v3 requests example working

This commit is contained in:
Rishabh Agarwal
2022-04-15 12:06:51 +05:30
committed by Andrew Bastin
parent 721a201e7a
commit bc2f81ff25
3 changed files with 170 additions and 5 deletions

View File

@@ -0,0 +1,62 @@
import { OpenAPIV3 } from "openapi-types"
import { pipe } from "fp-ts/function"
import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option"
type SchemaType = OpenAPIV3.ArraySchemaObjectType | OpenAPIV3.NonArraySchemaObjectType
type PrimitiveSchemaType = Exclude<SchemaType, "array" | "object">
type PrimitiveRequestBodyExampleType = string | number | boolean | null
type RequestBodyExampleType = PrimitiveRequestBodyExampleType | Array<RequestBodyExampleType> | { [name: string]: RequestBodyExampleType }
const isSchemaTypePrimitive = (schemaType: SchemaType) : schemaType is PrimitiveSchemaType => !["array", "object"].includes(schemaType)
const getPrimitiveTypePlaceholder = (primitiveType: PrimitiveSchemaType) : PrimitiveRequestBodyExampleType => {
switch(primitiveType) {
case "number": return 0.0
case "integer": return 0
case "string": return "string"
case "boolean": return true
}
}
// Use carefully, call only when type is primitive
// TODO(agarwal): Use Enum values, if any
const generatePrimitiveRequestBodyExample = (schemaObject: OpenAPIV3.NonArraySchemaObject) : RequestBodyExampleType =>
getPrimitiveTypePlaceholder(schemaObject.type as PrimitiveSchemaType)
// Use carefully, call only when type is object
const generateObjectRequestBodyExample = (schemaObject: OpenAPIV3.NonArraySchemaObject) : RequestBodyExampleType =>
pipe(
schemaObject.properties,
O.fromNullable,
O.map((properties) => Object.entries(properties) as [string, OpenAPIV3.SchemaObject][]),
O.getOrElseW(() => [] as [string, OpenAPIV3.SchemaObject][]),
A.reduce(
{} as {[name: string]: RequestBodyExampleType},
(aggregatedExample, property) => {
aggregatedExample[property[0]] = generateRequestBodyExampleFromSchemaObject(property[1])
return aggregatedExample
}
)
)
const generateArrayRequestBodyExample = (schemaObject: OpenAPIV3.ArraySchemaObject) : RequestBodyExampleType =>
Array.of(generateRequestBodyExampleFromSchemaObject(schemaObject.items as OpenAPIV3.SchemaObject))
const generateRequestBodyExampleFromSchemaObject = (schemaObject: OpenAPIV3.SchemaObject) : RequestBodyExampleType => {
// TODO: Handle schema objects with oneof or anyof
if(schemaObject.example) return schemaObject.example as RequestBodyExampleType
if(!schemaObject.type) return ""
if(isSchemaTypePrimitive(schemaObject.type)) return generatePrimitiveRequestBodyExample(schemaObject as OpenAPIV3.NonArraySchemaObject)
if(schemaObject.type === "object") return generateObjectRequestBodyExample(schemaObject as OpenAPIV3.NonArraySchemaObject)
return generateArrayRequestBodyExample(schemaObject as OpenAPIV3.ArraySchemaObject)
}
export const generateRequestBodyExampleFromMediaObject = (mediaObject: OpenAPIV3.MediaTypeObject) : RequestBodyExampleType => {
if(mediaObject.example) return mediaObject.example as RequestBodyExampleType
if(mediaObject.examples) return mediaObject.examples[0] as RequestBodyExampleType
return mediaObject.schema ? generateRequestBodyExampleFromSchemaObject(mediaObject.schema as OpenAPIV3.SchemaObject) : ""
}

View File

@@ -0,0 +1,84 @@
import { OpenAPIV3_1 as OpenAPIV31 } from "openapi-types"
import { pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
type MixedArraySchemaType = (OpenAPIV31.ArraySchemaObjectType | OpenAPIV31.NonArraySchemaObjectType)[]
type SchemaType = OpenAPIV31.ArraySchemaObjectType | OpenAPIV31.NonArraySchemaObjectType | MixedArraySchemaType
type PrimitiveSchemaType = Exclude<OpenAPIV31.NonArraySchemaObjectType, "object">
type PrimitiveRequestBodyExampleType = string | number | boolean | null
type RequestBodyExampleType = PrimitiveRequestBodyExampleType | Array<RequestBodyExampleType> | { [name: string]: RequestBodyExampleType }
const isSchemaTypePrimitive = (schemaType: SchemaType) : schemaType is PrimitiveSchemaType => !Array.isArray(schemaType) && !["array", "object"].includes(schemaType)
const getPrimitiveTypePlaceholder = (primitiveType: PrimitiveSchemaType) : PrimitiveRequestBodyExampleType => {
switch(primitiveType) {
case "number": return 0.0
case "integer": return 0
case "string": return "string"
case "boolean": return true
}
return null
}
// Use carefully, the schema type should necessarily be primitive
// TODO(agarwal): Use Enum values, if any
const generatePrimitiveRequestBodyExample = (schemaObject: OpenAPIV31.NonArraySchemaObject) : RequestBodyExampleType =>
getPrimitiveTypePlaceholder(schemaObject.type as PrimitiveSchemaType)
// Use carefully, the schema type should necessarily be object
const generateObjectRequestBodyExample = (schemaObject: OpenAPIV31.NonArraySchemaObject) : RequestBodyExampleType =>
pipe(
schemaObject.properties,
O.fromNullable,
O.map((properties) => Object.entries(properties) as [string, OpenAPIV31.SchemaObject][]),
O.getOrElseW(() => [] as [string, OpenAPIV31.SchemaObject][]),
A.reduce(
{} as {[name: string]: RequestBodyExampleType},
(aggregatedExample, property) => {
aggregatedExample[property[0]] = generateRequestBodyExampleFromSchemaObject(property[1])
return aggregatedExample
}
)
)
// Use carefully, the schema type should necessarily be mixed array
const generateMixedArrayRequestBodyEcample = (schemaObject: OpenAPIV31.SchemaObject) : RequestBodyExampleType =>
pipe(
schemaObject,
schemaObject => schemaObject.type as MixedArraySchemaType,
A.reduce(
[] as Array<RequestBodyExampleType>,
(aggregatedExample, itemType) => {
// TODO: Figure out how to include non-primitive types as well
if(isSchemaTypePrimitive(itemType)) {
aggregatedExample.push(getPrimitiveTypePlaceholder(itemType))
}
return aggregatedExample
}
)
)
const generateArrayRequestBodyExample = (schemaObject: OpenAPIV31.ArraySchemaObject) : RequestBodyExampleType =>
Array.of(generateRequestBodyExampleFromSchemaObject(schemaObject.items as OpenAPIV31.SchemaObject))
const generateRequestBodyExampleFromSchemaObject = (schemaObject: OpenAPIV31.SchemaObject) : RequestBodyExampleType => {
// TODO: Handle schema objects with oneof or anyof
if(schemaObject.example) return schemaObject.example as RequestBodyExampleType
if(schemaObject.examples) return schemaObject.examples[0] as RequestBodyExampleType
if(!schemaObject.type) return ""
if(isSchemaTypePrimitive(schemaObject.type)) return generatePrimitiveRequestBodyExample(schemaObject as OpenAPIV31.NonArraySchemaObject)
if(schemaObject.type === "object") return generateObjectRequestBodyExample(schemaObject)
if(schemaObject.type === "array") return generateArrayRequestBodyExample(schemaObject)
return generateMixedArrayRequestBodyEcample(schemaObject)
}
export const generateRequestBodyExampleFromMediaObject = (mediaObject: OpenAPIV31.MediaTypeObject) : RequestBodyExampleType => {
if(mediaObject.example) return mediaObject.example as RequestBodyExampleType
if(mediaObject.examples) return mediaObject.examples[0] as RequestBodyExampleType
return mediaObject.schema ? generateRequestBodyExampleFromSchemaObject(mediaObject.schema) : ""
}

View File

@@ -0,0 +1,835 @@
import {
OpenAPI,
OpenAPIV2,
OpenAPIV3,
OpenAPIV3_1 as OpenAPIV31,
} from "openapi-types"
import SwaggerParser from "@apidevtools/swagger-parser"
import yaml from "js-yaml"
import {
FormDataKeyValue,
HoppRESTAuth,
HoppRESTHeader,
HoppRESTParam,
HoppRESTReqBody,
HoppRESTRequest,
knownContentTypes,
makeRESTRequest,
HoppCollection,
makeCollection,
} from "@hoppscotch/data"
import { pipe, flow } from "fp-ts/function"
import * as A from "fp-ts/Array"
import * as S from "fp-ts/string"
import * as O from "fp-ts/Option"
import * as TE from "fp-ts/TaskEither"
import * as RA from "fp-ts/ReadonlyArray"
import { step } from "../../steps"
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "../"
import { generateRequestBodyExampleFromMediaObject as generateExampleV31 } from "./exampleV31"
import { generateRequestBodyExampleFromMediaObject as generateExampleV3 } from "./exampleV3"
export const OPENAPI_DEREF_ERROR = "openapi/deref_error" as const
// TODO: URL Import Support
const safeParseJSON = (str: string) => O.tryCatch(() => JSON.parse(str))
const safeParseYAML = (str: string) => O.tryCatch(() => yaml.load(str))
const objectHasProperty = <T extends string>(
obj: unknown,
propName: T
// eslint-disable-next-line
): obj is { [propName in T]: unknown } =>
!!obj &&
typeof obj === "object" &&
Object.prototype.hasOwnProperty.call(obj, propName)
type OpenAPIPathInfoType =
| OpenAPIV2.PathItemObject<{}>
| OpenAPIV3.PathItemObject<{}>
| OpenAPIV31.PathItemObject<{}>
type OpenAPIParamsType =
| OpenAPIV2.ParameterObject
| OpenAPIV3.ParameterObject
| OpenAPIV31.ParameterObject
type OpenAPIOperationType =
| OpenAPIV2.OperationObject
| OpenAPIV3.OperationObject
| OpenAPIV31.OperationObject
// Removes the OpenAPI Path Templating to the Hoppscotch Templating (<< ? >>)
const replaceOpenApiPathTemplating = flow(
S.replace(/{/g, "<<"),
S.replace(/}/g, ">>")
)
const parseOpenAPIParams = (params: OpenAPIParamsType[]): HoppRESTParam[] =>
pipe(
params,
A.filterMap(
flow(
O.fromPredicate((param) => param.in === "query"),
O.map(
(param) =>
<HoppRESTParam>{
key: param.name,
value: "", // TODO: Can we do anything more ? (parse default values maybe)
active: true,
}
)
)
)
)
const parseOpenAPIHeaders = (params: OpenAPIParamsType[]): HoppRESTHeader[] =>
pipe(
params,
A.filterMap(
flow(
O.fromPredicate((param) => param.in === "header"),
O.map(
(header) =>
<HoppRESTParam>{
key: header.name,
value: "", // TODO: Can we do anything more ? (parse default values maybe)
active: true,
}
)
)
)
)
const parseOpenAPIV2Body = (op: OpenAPIV2.OperationObject): HoppRESTReqBody => {
const obj = (op.consumes ?? [])[0] as string | undefined
// Not a content-type Hoppscotch supports
if (!obj || !(obj in knownContentTypes))
return { contentType: null, body: null }
// Textual Content Types, so we just parse it and keep
if (
obj !== "multipart/form-data" &&
obj !== "application/x-www-form-urlencoded"
) {
return {
contentType: obj as any,
body: generateRequestBodyExampleFromOpenAPIV2Body(op),
}
}
const formDataValues = pipe(
(op.parameters ?? []) as OpenAPIV2.Parameter[],
A.filterMap(
flow(
O.fromPredicate((param) => param.in === "body"),
O.map(
(param) =>
<FormDataKeyValue>{
key: param.name,
isFile: false,
value: "",
active: true,
}
)
)
)
)
return obj === "application/x-www-form-urlencoded"
? {
contentType: obj,
body: formDataValues.map(({ key }) => `${key}: `).join("\n"),
}
: { contentType: obj, body: formDataValues }
}
const parseOpenAPIV3BodyFormData = (
contentType: "multipart/form-data" | "application/x-www-form-urlencoded",
mediaObj: OpenAPIV3.MediaTypeObject | OpenAPIV31.MediaTypeObject
): HoppRESTReqBody => {
const schema = mediaObj.schema as
| OpenAPIV3.SchemaObject
| OpenAPIV31.SchemaObject
| undefined
if (!schema || schema.type !== "object") {
return contentType === "application/x-www-form-urlencoded"
? { contentType, body: "" }
: { contentType, body: [] }
}
const keys = Object.keys(schema.properties ?? {})
if (contentType === "application/x-www-form-urlencoded") {
return {
contentType,
body: keys.map((key) => `${key}: `).join("\n"),
}
} else {
return {
contentType,
body: keys.map(
(key) =>
<FormDataKeyValue>{ key, value: "", isFile: false, active: true }
),
}
}
}
type PrimitiveSchemaType = "string" | "integer" | "number" | "boolean"
type SchemaType = "array" | "object" | PrimitiveSchemaType
type PrimitiveRequestBodyExampleType = number | string | boolean
type RequestBodyExampleType =
| { [name: string]: RequestBodyExampleType }
| Array<RequestBodyExampleType>
| PrimitiveRequestBodyExampleType
const getPrimitiveTypePlaceholder = (
schemaType: PrimitiveSchemaType
): PrimitiveRequestBodyExampleType => {
switch (schemaType) {
case "string":
return "string"
case "integer":
case "number":
return 1
case "boolean":
return true
}
}
const getSchemaTypeFromSchemaObject = (
schema: OpenAPIV2.SchemaObject
): O.Option<SchemaType> =>
pipe(
schema.type,
O.fromNullable,
O.map(
(schemaType) =>
(Array.isArray(schemaType) ? schemaType[0] : schemaType) as SchemaType
)
)
const isSchemaTypePrimitive = (
schemaType: string
): schemaType is PrimitiveSchemaType =>
["string", "integer", "number", "boolean"].includes(schemaType)
const isSchemaTypeArray = (schemaType: string): schemaType is "array" =>
schemaType === "array"
const isSchemaTypeObject = (schemaType: string): schemaType is "object" =>
schemaType === "object"
const getSampleEnumValueOrPlaceholder = (
schema: OpenAPIV2.SchemaObject
): RequestBodyExampleType => {
const enumValue = pipe(
schema.enum,
O.fromNullable,
O.map((enums) => enums[0] as RequestBodyExampleType)
)
if (O.isSome(enumValue)) return enumValue.value
return pipe(
schema,
getSchemaTypeFromSchemaObject,
O.filter(isSchemaTypePrimitive),
O.map(getPrimitiveTypePlaceholder),
O.getOrElseW(() => "")
)
}
const generateExampleArrayFromOpenAPIV2ItemsObject = (
items: OpenAPIV2.ItemsObject
): RequestBodyExampleType => {
// ItemsObject can not hold type "object"
// https://swagger.io/specification/v2/#itemsObject
// TODO : Handle array of objects
// https://stackoverflow.com/questions/60490974/how-to-define-an-array-of-objects-in-openapi-2-0
const primitivePlaceholder = pipe(
items,
O.fromPredicate(
flow((items) => items.type as SchemaType, isSchemaTypePrimitive)
),
O.map(getSampleEnumValueOrPlaceholder)
)
if (O.isSome(primitivePlaceholder))
return Array.of(primitivePlaceholder.value, primitivePlaceholder.value)
// If the type is not primitive, it is "array"
// items property is required if type is array
return Array.of(
generateExampleArrayFromOpenAPIV2ItemsObject(
items.items as OpenAPIV2.ItemsObject
)
)
}
const generateRequestBodyExampleFromOpenAPIV2BodySchema = (
schema: OpenAPIV2.SchemaObject
): RequestBodyExampleType => {
if (schema.example) return schema.example as RequestBodyExampleType
const primitiveTypeExample = pipe(
schema,
O.fromPredicate(
flow(
getSchemaTypeFromSchemaObject,
O.map(isSchemaTypePrimitive),
O.getOrElseW(() => false) // No schema type found in the schema object, assume non-primitive
)
),
O.map(getSampleEnumValueOrPlaceholder) // Use enum or placeholder to populate primitive field
)
if (O.isSome(primitiveTypeExample)) return primitiveTypeExample.value
const arrayTypeExample = pipe(
schema,
O.fromPredicate(
flow(
getSchemaTypeFromSchemaObject,
O.map(isSchemaTypeArray),
O.getOrElseW(() => false) // No schema type found in the schema object, assume type to be different from array
)
),
O.map((schema) => schema.items as OpenAPIV2.ItemsObject),
O.map(generateExampleArrayFromOpenAPIV2ItemsObject)
)
if (O.isSome(arrayTypeExample)) return arrayTypeExample.value
return pipe(
schema,
O.fromPredicate(
flow(
getSchemaTypeFromSchemaObject,
O.map(isSchemaTypeObject),
O.getOrElseW(() => false)
)
),
O.chain((schema) =>
pipe(
schema.properties,
O.fromNullable,
O.map(
(properties) =>
Object.entries(properties) as [string, OpenAPIV2.SchemaObject][]
)
)
),
O.getOrElseW(() => [] as [string, OpenAPIV2.SchemaObject][]),
A.reduce(
{} as { [name: string]: RequestBodyExampleType },
(aggregatedExample, property) => {
const example = generateRequestBodyExampleFromOpenAPIV2BodySchema(
property[1]
)
aggregatedExample[property[0]] = example
return aggregatedExample
}
)
)
}
const getSchemaFromOpenAPIV2Parameter = (
parameter: OpenAPIV2.Parameter
): OpenAPIV2.SchemaObject => parameter.schema
const generateRequestBodyExampleFromOpenAPIV2Body = (
op: OpenAPIV2.OperationObject
): string =>
pipe(
(op.parameters ?? []) as OpenAPIV2.Parameter[],
A.findFirst((param) => param.in === "body"),
O.map(
flow(
getSchemaFromOpenAPIV2Parameter,
generateRequestBodyExampleFromOpenAPIV2BodySchema
)
),
O.getOrElse(() => "" as RequestBodyExampleType),
(requestBodyExample) => JSON.stringify(requestBodyExample, null, "\t") // Using a tab character mimics standard pretty-print appearance
)
const parseOpenAPIV3Body = (
op: OpenAPIV3.OperationObject | OpenAPIV31.OperationObject,
isV31Request: boolean
): HoppRESTReqBody => {
const objs = Object.entries(
(
op.requestBody as
| OpenAPIV3.RequestBodyObject
| OpenAPIV31.RequestBodyObject
| undefined
)?.content ?? {}
)
if (objs.length === 0) return { contentType: null, body: null }
// We only take the first definition
const [contentType, media]: [
string,
OpenAPIV3.MediaTypeObject | OpenAPIV31.MediaTypeObject
] = objs[0]
const exampleBody = JSON.stringify(
isV31Request
? generateExampleV31(media as OpenAPIV31.MediaTypeObject)
: generateExampleV3(media as OpenAPIV3.MediaTypeObject),
null,
"\t"
)
return contentType in knownContentTypes
? contentType === "multipart/form-data" ||
contentType === "application/x-www-form-urlencoded"
? parseOpenAPIV3BodyFormData(contentType, media)
: { contentType: contentType as any, body: exampleBody }
: { contentType: null, body: null }
}
const isOpenAPIV3Operation = (
doc: OpenAPI.Document,
op: OpenAPIOperationType
): op is OpenAPIV3.OperationObject | OpenAPIV31.OperationObject =>
objectHasProperty(doc, "openapi") &&
typeof doc.openapi === "string" &&
doc.openapi.startsWith("3.")
const isOpenAPIV31Operation = (
doc: OpenAPI.Document,
op: OpenAPIOperationType
): op is OpenAPIV31.OperationObject =>
objectHasProperty(doc, "openapi") &&
typeof doc.openapi === "string" &&
doc.openapi.startsWith("3.1")
const parseOpenAPIBody = (
doc: OpenAPI.Document,
op: OpenAPIOperationType
): HoppRESTReqBody =>
isOpenAPIV3Operation(doc, op)
? parseOpenAPIV3Body(op, isOpenAPIV31Operation(doc, op))
: parseOpenAPIV2Body(op)
const resolveOpenAPIV3SecurityObj = (
scheme: OpenAPIV3.SecuritySchemeObject | OpenAPIV31.SecuritySchemeObject,
_schemeData: string[] // Used for OAuth to pass params
): HoppRESTAuth => {
if (scheme.type === "http") {
if (scheme.scheme === "basic") {
// Basic
return { authType: "basic", authActive: true, username: "", password: "" }
} else if (scheme.scheme === "bearer") {
// Bearer
return { authType: "bearer", authActive: true, token: "" }
} else {
// Unknown/Unsupported Scheme
return { authType: "none", authActive: true }
}
} else if (scheme.type === "apiKey") {
if (scheme.in === "header") {
return {
authType: "api-key",
authActive: true,
addTo: "Headers",
key: scheme.name,
value: "",
}
} else if (scheme.in === "query") {
return {
authType: "api-key",
authActive: true,
addTo: "Query params",
key: scheme.in,
value: "",
}
}
} else if (scheme.type === "oauth2") {
// NOTE: We select flow on a first come basis on this order, authorizationCode > implicit > password > clientCredentials
if (scheme.flows.authorizationCode) {
return {
authType: "oauth-2",
authActive: true,
accessTokenURL: scheme.flows.authorizationCode.tokenUrl ?? "",
authURL: scheme.flows.authorizationCode.authorizationUrl ?? "",
clientID: "",
oidcDiscoveryURL: "",
scope: _schemeData.join(" "),
token: "",
}
} else if (scheme.flows.implicit) {
return {
authType: "oauth-2",
authActive: true,
authURL: scheme.flows.implicit.authorizationUrl ?? "",
accessTokenURL: "",
clientID: "",
oidcDiscoveryURL: "",
scope: _schemeData.join(" "),
token: "",
}
} else if (scheme.flows.password) {
return {
authType: "oauth-2",
authActive: true,
authURL: "",
accessTokenURL: scheme.flows.password.tokenUrl ?? "",
clientID: "",
oidcDiscoveryURL: "",
scope: _schemeData.join(" "),
token: "",
}
} else if (scheme.flows.clientCredentials) {
return {
authType: "oauth-2",
authActive: true,
accessTokenURL: scheme.flows.clientCredentials.tokenUrl ?? "",
authURL: "",
clientID: "",
oidcDiscoveryURL: "",
scope: _schemeData.join(" "),
token: "",
}
} else {
return {
authType: "oauth-2",
authActive: true,
accessTokenURL: "",
authURL: "",
clientID: "",
oidcDiscoveryURL: "",
scope: _schemeData.join(" "),
token: "",
}
}
} else if (scheme.type === "openIdConnect") {
return {
authType: "oauth-2",
authActive: true,
accessTokenURL: "",
authURL: "",
clientID: "",
oidcDiscoveryURL: scheme.openIdConnectUrl ?? "",
scope: _schemeData.join(" "),
token: "",
}
}
return { authType: "none", authActive: true }
}
const resolveOpenAPIV3SecurityScheme = (
doc: OpenAPIV3.Document | OpenAPIV31.Document,
schemeName: string,
schemeData: string[]
): HoppRESTAuth => {
const scheme = doc.components?.securitySchemes?.[schemeName] as
| OpenAPIV3.SecuritySchemeObject
| undefined
if (!scheme) return { authType: "none", authActive: true }
else return resolveOpenAPIV3SecurityObj(scheme, schemeData)
}
const resolveOpenAPIV3Security = (
doc: OpenAPIV3.Document | OpenAPIV31.Document,
security:
| OpenAPIV3.SecurityRequirementObject[]
| OpenAPIV31.SecurityRequirementObject[]
): HoppRESTAuth => {
// NOTE: Hoppscotch only considers the first security requirement
const sec = security[0] as OpenAPIV3.SecurityRequirementObject | undefined
if (!sec) return { authType: "none", authActive: true }
// NOTE: We only consider the first security condition within the first condition
const [schemeName, schemeData] = (Object.entries(sec)[0] ?? [
undefined,
undefined,
]) as [string | undefined, string[] | undefined]
if (!schemeName || !schemeData) return { authType: "none", authActive: true }
return resolveOpenAPIV3SecurityScheme(doc, schemeName, schemeData)
}
const parseOpenAPIV3Auth = (
doc: OpenAPIV3.Document | OpenAPIV31.Document,
op: OpenAPIV3.OperationObject | OpenAPIV31.OperationObject
): HoppRESTAuth => {
const rootAuth = doc.security
? resolveOpenAPIV3Security(doc, doc.security)
: undefined
const opAuth = op.security
? resolveOpenAPIV3Security(doc, op.security)
: undefined
return opAuth ?? rootAuth ?? { authType: "none", authActive: true }
}
const resolveOpenAPIV2SecurityScheme = (
scheme: OpenAPIV2.SecuritySchemeObject,
_schemeData: string[]
): HoppRESTAuth => {
if (scheme.type === "basic") {
return { authType: "basic", authActive: true, username: "", password: "" }
} else if (scheme.type === "apiKey") {
// V2 only supports in: header and in: query
return {
authType: "api-key",
addTo: scheme.in === "header" ? "Headers" : "Query params",
authActive: true,
key: scheme.name,
value: "",
}
} else if (scheme.type === "oauth2") {
// NOTE: We select flow on a first come basis on this order, accessCode > implicit > password > application
if (scheme.flow === "accessCode") {
return {
authType: "oauth-2",
authActive: true,
accessTokenURL: scheme.tokenUrl ?? "",
authURL: scheme.authorizationUrl ?? "",
clientID: "",
oidcDiscoveryURL: "",
scope: _schemeData.join(" "),
token: "",
}
} else if (scheme.flow === "implicit") {
return {
authType: "oauth-2",
authActive: true,
accessTokenURL: "",
authURL: scheme.authorizationUrl ?? "",
clientID: "",
oidcDiscoveryURL: "",
scope: _schemeData.join(" "),
token: "",
}
} else if (scheme.flow === "application") {
return {
authType: "oauth-2",
authActive: true,
accessTokenURL: scheme.tokenUrl ?? "",
authURL: "",
clientID: "",
oidcDiscoveryURL: "",
scope: _schemeData.join(" "),
token: "",
}
} else if (scheme.flow === "password") {
return {
authType: "oauth-2",
authActive: true,
accessTokenURL: scheme.tokenUrl ?? "",
authURL: "",
clientID: "",
oidcDiscoveryURL: "",
scope: _schemeData.join(" "),
token: "",
}
} else {
return {
authType: "oauth-2",
authActive: true,
accessTokenURL: "",
authURL: "",
clientID: "",
oidcDiscoveryURL: "",
scope: _schemeData.join(" "),
token: "",
}
}
}
return { authType: "none", authActive: true }
}
const resolveOpenAPIV2SecurityDef = (
doc: OpenAPIV2.Document,
schemeName: string,
schemeData: string[]
): HoppRESTAuth => {
const scheme = Object.entries(doc.securityDefinitions ?? {}).find(
([name]) => schemeName === name
)
if (!scheme) return { authType: "none", authActive: true }
const schemeObj = scheme[1]
return resolveOpenAPIV2SecurityScheme(schemeObj, schemeData)
}
const resolveOpenAPIV2Security = (
doc: OpenAPIV2.Document,
security: OpenAPIV2.SecurityRequirementObject[]
): HoppRESTAuth => {
// NOTE: Hoppscotch only considers the first security requirement
const sec = security[0] as OpenAPIV2.SecurityRequirementObject | undefined
if (!sec) return { authType: "none", authActive: true }
// NOTE: We only consider the first security condition within the first condition
const [schemeName, schemeData] = (Object.entries(sec)[0] ?? [
undefined,
undefined,
]) as [string | undefined, string[] | undefined]
if (!schemeName || !schemeData) return { authType: "none", authActive: true }
return resolveOpenAPIV2SecurityDef(doc, schemeName, schemeData)
}
const parseOpenAPIV2Auth = (
doc: OpenAPIV2.Document,
op: OpenAPIV2.OperationObject
): HoppRESTAuth => {
const rootAuth = doc.security
? resolveOpenAPIV2Security(doc, doc.security)
: undefined
const opAuth = op.security
? resolveOpenAPIV2Security(doc, op.security)
: undefined
return opAuth ?? rootAuth ?? { authType: "none", authActive: true }
}
const parseOpenAPIAuth = (
doc: OpenAPI.Document,
op: OpenAPIOperationType
): HoppRESTAuth =>
isOpenAPIV3Operation(doc, op)
? parseOpenAPIV3Auth(doc as OpenAPIV3.Document | OpenAPIV31.Document, op)
: parseOpenAPIV2Auth(doc as OpenAPIV2.Document, op)
const convertPathToHoppReqs = (
doc: OpenAPI.Document,
pathName: string,
pathObj: OpenAPIPathInfoType
) =>
pipe(
["get", "head", "post", "put", "delete", "options", "patch"] as const,
// Filter and map out path info
RA.filterMap(
flow(
O.fromPredicate((method) => !!pathObj[method]),
O.map((method) => ({ method, info: pathObj[method]! }))
)
),
// Construct request object
RA.map(({ method, info }) =>
makeRESTRequest({
name: info.operationId ?? info.summary ?? "Untitled Request",
method: method.toUpperCase(),
endpoint: `<<baseUrl>>${replaceOpenApiPathTemplating(pathName)}`, // TODO: Make this proper
// We don't need to worry about reference types as the Dereferencing pass should remove them
params: parseOpenAPIParams(
(info.parameters as OpenAPIParamsType[] | undefined) ?? []
),
headers: parseOpenAPIHeaders(
(info.parameters as OpenAPIParamsType[] | undefined) ?? []
),
auth: parseOpenAPIAuth(doc, info),
body: parseOpenAPIBody(doc, info),
preRequestScript: "",
testScript: "",
})
),
// Disable Readonly
RA.toArray
)
const convertOpenApiDocToHopp = (
doc: OpenAPI.Document
): TE.TaskEither<never, HoppCollection<HoppRESTRequest>[]> => {
const name = doc.info.title
const paths = Object.entries(doc.paths ?? {})
.map(([pathName, pathObj]) => convertPathToHoppReqs(doc, pathName, pathObj))
.flat()
return TE.of([
makeCollection<HoppRESTRequest>({
name,
folders: [],
requests: paths,
}),
])
}
const parseOpenAPIDocContent = (str: string) =>
pipe(
str,
safeParseJSON,
O.match(
() => safeParseYAML(str),
(data) => O.of(data)
)
)
export default defineImporter({
id: "openapi",
name: "import.from_openapi",
applicableTo: ["my-collections", "team-collections", "url-import"],
icon: "file",
steps: [
step({
stepName: "FILE_IMPORT",
metadata: {
caption: "import.from_openapi_description",
acceptedFileTypes: ".json, .yaml, .yml",
},
}),
] as const,
importer: ([fileContent]) =>
pipe(
// See if we can parse JSON properly
fileContent,
parseOpenAPIDocContent,
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT),
// Try validating, else the importer is invalid file format
TE.chainW((obj) =>
pipe(
TE.tryCatch(
() => SwaggerParser.validate(obj),
() => IMPORTER_INVALID_FILE_FORMAT
)
)
),
// Deference the references
TE.chainW((obj) =>
pipe(
TE.tryCatch(
() => SwaggerParser.dereference(obj),
() => OPENAPI_DEREF_ERROR
)
)
),
TE.chainW(convertOpenApiDocToHopp)
),
})