diff --git a/packages/hoppscotch-app/helpers/functional/json.ts b/packages/hoppscotch-app/helpers/functional/json.ts index 79cf9cba4..1f263e7f6 100644 --- a/packages/hoppscotch-app/helpers/functional/json.ts +++ b/packages/hoppscotch-app/helpers/functional/json.ts @@ -9,6 +9,14 @@ import { flow } from "fp-ts/function" export const safeParseJSON = (str: string): O.Option => O.tryCatch(() => JSON.parse(str)) +/** + * Generates a prettified JSON representation of an object + * @param obj The object to get the representation of + * @returns The prettified JSON string of the object + */ +export const prettyPrintJSON = (obj: unknown): O.Option => + O.tryCatch(() => JSON.stringify(obj, null, "\t")) + /** * Checks if given string is a JSON string * @param str Raw string to be checked diff --git a/packages/hoppscotch-app/helpers/import-export/import/openapi/exampleV2.ts b/packages/hoppscotch-app/helpers/import-export/import/openapi/exampleV2.ts new file mode 100644 index 000000000..553519d38 --- /dev/null +++ b/packages/hoppscotch-app/helpers/import-export/import/openapi/exampleV2.ts @@ -0,0 +1,185 @@ +import { OpenAPIV2 } from "openapi-types" +import * as O from "fp-ts/Option" +import { pipe, flow } from "fp-ts/function" +import * as A from "fp-ts/Array" +import { prettyPrintJSON } from "~/helpers/functional/json" + +type PrimitiveSchemaType = "string" | "integer" | "number" | "boolean" + +type SchemaType = "array" | "object" | PrimitiveSchemaType + +type PrimitiveRequestBodyExample = number | string | boolean + +type RequestBodyExample = + | { [name: string]: RequestBodyExample } + | Array + | PrimitiveRequestBodyExample + +const getPrimitiveTypePlaceholder = ( + schemaType: PrimitiveSchemaType +): PrimitiveRequestBodyExample => { + switch (schemaType) { + case "string": + return "string" + case "integer": + case "number": + return 1 + case "boolean": + return true + } +} + +const getSchemaTypeFromSchemaObject = ( + schema: OpenAPIV2.SchemaObject +): O.Option => + 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 +): RequestBodyExample => + pipe( + schema.enum, + O.fromNullable, + O.map((enums) => enums[0] as RequestBodyExample), + O.altW(() => + pipe( + schema, + getSchemaTypeFromSchemaObject, + O.filter(isSchemaTypePrimitive), + O.map(getPrimitiveTypePlaceholder) + ) + ), + O.getOrElseW(() => "") + ) + +const generateExampleArrayFromOpenAPIV2ItemsObject = ( + items: OpenAPIV2.ItemsObject +): RequestBodyExample => + // 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 + + pipe( + items, + O.fromPredicate( + flow((items) => items.type as SchemaType, isSchemaTypePrimitive) + ), + O.map( + flow(getSampleEnumValueOrPlaceholder, (arrayItem) => [ + arrayItem, + arrayItem, + ]) + ), + O.getOrElse(() => + // If the type is not primitive, it is "array" + // items property is required if type is array + [ + generateExampleArrayFromOpenAPIV2ItemsObject( + items.items as OpenAPIV2.ItemsObject + ), + ] + ) + ) + +const generateRequestBodyExampleFromOpenAPIV2BodySchema = ( + schema: OpenAPIV2.SchemaObject +): RequestBodyExample => { + if (schema.example) return schema.example as RequestBodyExample + + 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]: RequestBodyExample }, + (aggregatedExample, property) => { + const example = generateRequestBodyExampleFromOpenAPIV2BodySchema( + property[1] + ) + aggregatedExample[property[0]] = example + return aggregatedExample + } + ) + ) +} + +export const generateRequestBodyExampleFromOpenAPIV2Body = ( + op: OpenAPIV2.OperationObject +): string => + pipe( + (op.parameters ?? []) as OpenAPIV2.Parameter[], + A.findFirst((param) => param.in === "body"), + O.map( + flow( + (parameter) => parameter.schema, + generateRequestBodyExampleFromOpenAPIV2BodySchema + ) + ), + O.chain(prettyPrintJSON), + O.getOrElse(() => "") + ) diff --git a/packages/hoppscotch-app/helpers/import-export/import/openapi/exampleV3.ts b/packages/hoppscotch-app/helpers/import-export/import/openapi/exampleV3.ts new file mode 100644 index 000000000..f750377e8 --- /dev/null +++ b/packages/hoppscotch-app/helpers/import-export/import/openapi/exampleV3.ts @@ -0,0 +1,109 @@ +import { OpenAPIV3 } from "openapi-types" +import { pipe } from "fp-ts/function" +import * as O from "fp-ts/Option" +import { tupleToRecord } from "~/helpers/functional/record" + +type SchemaType = + | OpenAPIV3.ArraySchemaObjectType + | OpenAPIV3.NonArraySchemaObjectType + +type PrimitiveSchemaType = Exclude + +type PrimitiveRequestBodyExample = string | number | boolean | null + +type RequestBodyExample = + | PrimitiveRequestBodyExample + | Array + | { [name: string]: RequestBodyExample } + +const isSchemaTypePrimitive = ( + schemaType: SchemaType +): schemaType is PrimitiveSchemaType => + !["array", "object"].includes(schemaType) + +const getPrimitiveTypePlaceholder = ( + primitiveType: PrimitiveSchemaType +): PrimitiveRequestBodyExample => { + 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 +): RequestBodyExample => + getPrimitiveTypePlaceholder(schemaObject.type as PrimitiveSchemaType) + +// Use carefully, call only when type is object +const generateObjectRequestBodyExample = ( + schemaObject: OpenAPIV3.NonArraySchemaObject +): RequestBodyExample => + pipe( + schemaObject.properties, + O.fromNullable, + O.map(Object.entries), + O.getOrElseW(() => [] as [string, OpenAPIV3.SchemaObject][]), + tupleToRecord + ) + +const generateArrayRequestBodyExample = ( + schemaObject: OpenAPIV3.ArraySchemaObject +): RequestBodyExample => [ + generateRequestBodyExampleFromSchemaObject( + schemaObject.items as OpenAPIV3.SchemaObject + ), +] + +const generateRequestBodyExampleFromSchemaObject = ( + schemaObject: OpenAPIV3.SchemaObject +): RequestBodyExample => { + // TODO: Handle schema objects with allof + if (schemaObject.example) return schemaObject.example as RequestBodyExample + + // If request body can be oneof or allof several schema, choose the first schema to generate an example + if (schemaObject.oneOf) + return generateRequestBodyExampleFromSchemaObject( + schemaObject.oneOf[0] as OpenAPIV3.SchemaObject + ) + if (schemaObject.anyOf) + return generateRequestBodyExampleFromSchemaObject( + schemaObject.anyOf[0] as OpenAPIV3.SchemaObject + ) + + 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 +): RequestBodyExample => { + if (mediaObject.example) return mediaObject.example as RequestBodyExample + if (mediaObject.examples) return mediaObject.examples[0] as RequestBodyExample + return mediaObject.schema + ? generateRequestBodyExampleFromSchemaObject( + mediaObject.schema as OpenAPIV3.SchemaObject + ) + : "" +} diff --git a/packages/hoppscotch-app/helpers/import-export/import/openapi/exampleV31.ts b/packages/hoppscotch-app/helpers/import-export/import/openapi/exampleV31.ts new file mode 100644 index 000000000..31f2d2f11 --- /dev/null +++ b/packages/hoppscotch-app/helpers/import-export/import/openapi/exampleV31.ts @@ -0,0 +1,129 @@ +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 PrimitiveRequestBodyExample = string | number | boolean | null + +type RequestBodyExample = + | PrimitiveRequestBodyExample + | Array + | { [name: string]: RequestBodyExample } + +const isSchemaTypePrimitive = ( + schemaType: SchemaType +): schemaType is PrimitiveSchemaType => + !Array.isArray(schemaType) && !["array", "object"].includes(schemaType) + +const getPrimitiveTypePlaceholder = ( + primitiveType: PrimitiveSchemaType +): PrimitiveRequestBodyExample => { + 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 +): RequestBodyExample => + getPrimitiveTypePlaceholder(schemaObject.type as PrimitiveSchemaType) + +// Use carefully, the schema type should necessarily be object +const generateObjectRequestBodyExample = ( + schemaObject: OpenAPIV31.NonArraySchemaObject +): RequestBodyExample => + 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]: RequestBodyExample }, + (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 +): RequestBodyExample => + pipe( + schemaObject, + (schemaObject) => schemaObject.type as MixedArraySchemaType, + A.reduce([] as Array, (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 +): RequestBodyExample => [ + generateRequestBodyExampleFromSchemaObject( + schemaObject.items as OpenAPIV31.SchemaObject + ), +] + +const generateRequestBodyExampleFromSchemaObject = ( + schemaObject: OpenAPIV31.SchemaObject +): RequestBodyExample => { + // TODO: Handle schema objects with oneof or anyof + if (schemaObject.example) return schemaObject.example as RequestBodyExample + if (schemaObject.examples) + return schemaObject.examples[0] as RequestBodyExample + 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 +): RequestBodyExample => { + if (mediaObject.example) return mediaObject.example as RequestBodyExample + if (mediaObject.examples) return mediaObject.examples[0] as RequestBodyExample + return mediaObject.schema + ? generateRequestBodyExampleFromSchemaObject(mediaObject.schema) + : "" +} diff --git a/packages/hoppscotch-app/helpers/import-export/import/openapi.ts b/packages/hoppscotch-app/helpers/import-export/import/openapi/index.ts similarity index 93% rename from packages/hoppscotch-app/helpers/import-export/import/openapi.ts rename to packages/hoppscotch-app/helpers/import-export/import/openapi/index.ts index cdef55b21..ac5168922 100644 --- a/packages/hoppscotch-app/helpers/import-export/import/openapi.ts +++ b/packages/hoppscotch-app/helpers/import-export/import/openapi/index.ts @@ -24,8 +24,12 @@ 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 { step } from "../../steps" +import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "../" +import { generateRequestBodyExampleFromMediaObject as generateExampleV31 } from "./exampleV31" +import { generateRequestBodyExampleFromMediaObject as generateExampleV3 } from "./exampleV3" +import { generateRequestBodyExampleFromOpenAPIV2Body } from "./exampleV2" +import { prettyPrintJSON } from "~/helpers/functional/json" export const OPENAPI_DEREF_ERROR = "openapi/deref_error" as const @@ -114,8 +118,12 @@ const parseOpenAPIV2Body = (op: OpenAPIV2.OperationObject): HoppRESTReqBody => { if ( obj !== "multipart/form-data" && obj !== "application/x-www-form-urlencoded" - ) - return { contentType: obj as any, body: "" } + ) { + return { + contentType: obj as any, + body: generateRequestBodyExampleFromOpenAPIV2Body(op), + } + } const formDataValues = pipe( (op.parameters ?? []) as OpenAPIV2.Parameter[], @@ -178,7 +186,8 @@ const parseOpenAPIV3BodyFormData = ( } const parseOpenAPIV3Body = ( - op: OpenAPIV3.OperationObject | OpenAPIV31.OperationObject + op: OpenAPIV3.OperationObject | OpenAPIV31.OperationObject, + isV31Request: boolean ): HoppRESTReqBody => { const objs = Object.entries( ( @@ -197,11 +206,20 @@ const parseOpenAPIV3Body = ( OpenAPIV3.MediaTypeObject | OpenAPIV31.MediaTypeObject ] = objs[0] + const exampleBody = pipe( + prettyPrintJSON( + isV31Request + ? generateExampleV31(media as OpenAPIV31.MediaTypeObject) + : generateExampleV3(media as OpenAPIV3.MediaTypeObject) + ), + O.getOrElse(() => "") + ) + return contentType in knownContentTypes ? contentType === "multipart/form-data" || contentType === "application/x-www-form-urlencoded" ? parseOpenAPIV3BodyFormData(contentType, media) - : { contentType: contentType as any, body: "" } + : { contentType: contentType as any, body: exampleBody } : { contentType: null, body: null } } @@ -213,12 +231,20 @@ const isOpenAPIV3Operation = ( 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) + ? parseOpenAPIV3Body(op, isOpenAPIV31Operation(doc, op)) : parseOpenAPIV2Body(op) const resolveOpenAPIV3SecurityObj = (