123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- 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
- )
- )
- )
- )
- )
- }
|