Files
hoppscotch/packages/hoppscotch-app/helpers/curl/sub_helpers/body.ts

170 lines
4.7 KiB
TypeScript

import parser from "yargs-parser"
import { pipe, flow } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import * as RNEA from "fp-ts/ReadonlyNonEmptyArray"
import * as S from "fp-ts/string"
import {
HoppRESTReqBody,
HoppRESTReqBodyFormData,
ValidContentTypes,
knownContentTypes,
} from "@hoppscotch/data"
import { detectContentType, parseBody } from "./contentParser"
import { tupleToRecord } from "~/helpers/functional/record"
import {
objHasProperty,
objHasArrayProperty,
} from "~/helpers/functional/object"
type BodyReturnType =
| { type: "FORMDATA"; body: Record<string, string> }
| {
type: "NON_FORMDATA"
body: Exclude<HoppRESTReqBody, HoppRESTReqBodyFormData>
}
/** Parses body based on the content type
* @param rData Raw data
* @param cType Sanitized content type
* @returns Option of parsed body of type string | Record<string, string>
*/
const getBodyFromContentType =
(rData: string, cType: HoppRESTReqBody["contentType"]) => (rct: string) =>
pipe(
cType,
O.fromPredicate((ctype) => ctype === "multipart/form-data"),
O.chain(() =>
pipe(
// pass rawContentType for boundary ascertion
parseBody(rData, cType, rct),
O.filter((parsedBody) => typeof parsedBody !== "string")
)
),
O.alt(() =>
pipe(
parseBody(rData, cType),
O.filter(
(parsedBody) =>
typeof parsedBody === "string" && parsedBody.length > 0
)
)
)
)
const getContentTypeFromRawContentType = (rawContentType: string) =>
pipe(
rawContentType,
O.fromPredicate((rct) => rct.length > 0),
// get everything before semi-colon
O.map(flow(S.toLowerCase, S.split(";"), RNEA.head)),
// if rawContentType is valid, cast it to contentType type
O.filter((ct) => Object.keys(knownContentTypes).includes(ct)),
O.map((ct) => ct as HoppRESTReqBody["contentType"])
)
const getContentTypeFromRawData = (rawData: string) =>
pipe(
rawData,
O.fromPredicate((rd) => rd.length > 0),
O.map(detectContentType)
)
export const getBody = (
rawData: string,
rawContentType: string,
contentType: HoppRESTReqBody["contentType"]
): O.Option<BodyReturnType> => {
return pipe(
O.Do,
O.bind("cType", () =>
pipe(
// get provided content-type
contentType,
O.fromNullable,
// or figure it out
O.alt(() => getContentTypeFromRawContentType(rawContentType)),
O.alt(() => getContentTypeFromRawData(rawData))
)
),
O.bind("rData", () =>
pipe(
rawData,
O.fromPredicate(() => rawData.length > 0)
)
),
O.bind("ctBody", ({ cType, rData }) =>
pipe(rawContentType, getBodyFromContentType(rData, cType))
),
O.map(({ cType, ctBody }) =>
typeof ctBody === "string"
? {
type: "NON_FORMDATA",
body: {
body: ctBody,
contentType: cType as Exclude<
ValidContentTypes,
"multipart/form-data"
>,
},
}
: { type: "FORMDATA", body: ctBody }
)
)
}
/**
* Parses and structures multipart/form-data from -F argument of curl command
* @param parsedArguments Parsed Arguments object
* @returns Option of Record<string, string> type containing key-value pairs of multipart/form-data
*/
export function getFArgumentMultipartData(
parsedArguments: parser.Arguments
): O.Option<Record<string, string>> {
// --form or -F multipart data
return pipe(
parsedArguments,
// make it an array if not already
O.fromPredicate(objHasProperty("F", "string")),
O.map((args) => [args.F]),
O.alt(() =>
pipe(
parsedArguments,
O.fromPredicate(objHasArrayProperty("F", "string")),
O.map((args) => args.F)
)
),
O.chain(
flow(
A.map(S.split("=")),
// can only have a key and no value
O.fromPredicate((fArgs) => fArgs.length > 0),
O.map(
flow(
A.map(([k, v]) =>
pipe(
parsedArguments,
// form-string option allows for "@" and "<" prefixes
// without them being considered as files
O.fromPredicate(objHasProperty("form-string", "boolean")),
O.match(
// leave the value field empty for files
() => [k, v[0] === "@" || v[0] === "<" ? "" : v],
(_) => [k, v]
)
)
),
A.map(([k, v]) => [k, v] as [string, string]),
tupleToRecord
)
)
)
)
)
}