refactor: inherit default curl parser values (#2169)

This commit is contained in:
kyteinsky
2022-04-04 21:38:12 +05:30
committed by GitHub
parent dcbc3b6356
commit eea8a44746
16 changed files with 1354 additions and 977 deletions

View File

@@ -0,0 +1,116 @@
import { HoppRESTAuth } from "@hoppscotch/data"
import parser from "yargs-parser"
import * as O from "fp-ts/Option"
import * as S from "fp-ts/string"
import { pipe } from "fp-ts/function"
import { getDefaultRESTRequest } from "~/newstore/RESTSession"
import { objHasProperty } from "~/helpers/functional/object"
const defaultRESTReq = getDefaultRESTRequest()
const getAuthFromAuthHeader = (headers: Record<string, string>) =>
pipe(
headers.Authorization,
O.fromNullable,
O.map((a) => a.split(" ")),
O.filter((a) => a.length > 1),
O.chain((kv) =>
O.fromNullable(
(() => {
switch (kv[0].toLowerCase()) {
case "bearer":
return <HoppRESTAuth>{
authActive: true,
authType: "bearer",
token: kv[1],
}
case "basic": {
const [username, password] = pipe(
O.tryCatch(() => atob(kv[1])),
O.map(S.split(":")),
// can have a username with no password
O.filter((arr) => arr.length > 0),
O.map(
([username, password]) =>
<[string, string]>[username, password]
),
O.getOrElse(() => ["", ""])
)
if (!username) return undefined
return <HoppRESTAuth>{
authActive: true,
authType: "basic",
username,
password: password ?? "",
}
}
default:
return undefined
}
})()
)
)
)
const getAuthFromParsedArgs = (parsedArguments: parser.Arguments) =>
pipe(
parsedArguments,
O.fromPredicate(objHasProperty("u", "string")),
O.chain((args) =>
pipe(
args.u,
S.split(":"),
// can have a username with no password
O.fromPredicate((arr) => arr.length > 0 && arr[0].length > 0),
O.map(
([username, password]) => <[string, string]>[username, password ?? ""]
)
)
),
O.map(
([username, password]) =>
<HoppRESTAuth>{
authActive: true,
authType: "basic",
username,
password,
}
)
)
const getAuthFromURLObject = (urlObject: URL) =>
pipe(
urlObject,
(url) => [url.username, url.password ?? ""],
// can have a username with no password
O.fromPredicate(([username, _]) => !!username && username.length > 0),
O.map(
([username, password]) =>
<HoppRESTAuth>{
authActive: true,
authType: "basic",
username,
password,
}
)
)
/**
* Preference order:
* - Auth headers
* - --user or -u argument
* - Creds provided along with URL
*/
export const getAuthObject = (
parsedArguments: parser.Arguments,
headers: Record<string, string>,
urlObject: URL
): HoppRESTAuth =>
pipe(
getAuthFromAuthHeader(headers),
O.alt(() => getAuthFromParsedArgs(parsedArguments)),
O.alt(() => getAuthFromURLObject(urlObject)),
O.getOrElse(() => defaultRESTReq.auth)
)

View File

@@ -0,0 +1,169 @@
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
)
)
)
)
)
}

View File

@@ -0,0 +1,303 @@
import { HoppRESTReqBody } from "@hoppscotch/data"
import * as O from "fp-ts/Option"
import * as RA from "fp-ts/ReadonlyArray"
import * as S from "fp-ts/string"
import { pipe, flow } from "fp-ts/function"
import { tupleToRecord } from "~/helpers/functional/record"
import { safeParseJSON } from "~/helpers/functional/json"
import { optionChoose } from "~/helpers/functional/option"
const isJSON = flow(safeParseJSON, O.isSome)
const isXML = (rawData: string) =>
pipe(
rawData,
O.fromPredicate(() => /<\/?[a-zA-Z][\s\S]*>/i.test(rawData)),
O.chain(prettifyXml),
O.isSome
)
const isHTML = (rawData: string) =>
pipe(
rawData,
O.fromPredicate(() => /<\/?[a-zA-Z][\s\S]*>/i.test(rawData)),
O.isSome
)
const isFormData = (rawData: string) =>
pipe(
rawData.match(/^-{2,}[A-Za-z0-9]+\\r\\n/),
O.fromNullable,
O.filter((boundaryMatch) => boundaryMatch.length > 0),
O.isSome
)
const isXWWWFormUrlEncoded = (rawData: string) =>
pipe(
rawData,
O.fromPredicate((rd) => /([^&=]+)=([^&=]*)/.test(rd)),
O.isSome
)
/**
* Detects the content type of the input string
* @param rawData String for which content type is to be detected
* @returns Content type of the data
*/
export const detectContentType = (
rawData: string
): HoppRESTReqBody["contentType"] =>
pipe(
rawData,
optionChoose([
[(rd) => !rd, null],
[isJSON, "application/json" as const],
[isFormData, "multipart/form-data" as const],
[isXML, "application/xml" as const],
[isHTML, "text/html" as const],
[isXWWWFormUrlEncoded, "application/x-www-form-urlencoded" as const],
]),
O.getOrElseW(() => "text/plain" as const)
)
const multipartFunctions = {
getBoundary(rawData: string, rawContentType: string | undefined) {
return pipe(
rawContentType,
O.fromNullable,
O.filter((rct) => rct.length > 0),
O.match(
() => this.getBoundaryFromRawData(rawData),
(rct) => this.getBoundaryFromRawContentType(rawData, rct)
)
)
},
getBoundaryFromRawData(rawData: string) {
return pipe(
rawData.match(/(-{2,}[A-Za-z0-9]+)\\r\\n/g),
O.fromNullable,
O.filter((boundaryMatch) => boundaryMatch.length > 0),
O.map((matches) => matches[0].slice(0, -4))
)
},
getBoundaryFromRawContentType(rawData: string, rawContentType: string) {
return pipe(
rawContentType.match(/boundary=(.+)/),
O.fromNullable,
O.filter((boundaryContentMatch) => boundaryContentMatch.length > 1),
O.filter((matches) =>
rawData.replaceAll("\\r\\n", "").endsWith("--" + matches[1] + "--")
),
O.map((matches) => "--" + matches[1])
)
},
splitUsingBoundaryAndNewLines(rawData: string, boundary: string) {
return pipe(
rawData,
S.split(RegExp(`${boundary}-*`)),
RA.filter((p) => p !== "" && p.includes("name")),
RA.map((p) =>
pipe(
p.replaceAll(/\\r\\n+/g, "\\r\\n"),
S.split("\\r\\n"),
RA.filter((q) => q !== "")
)
)
)
},
getNameValuePair(pair: readonly string[]) {
return pipe(
pair,
O.fromPredicate((p) => p.length > 1),
O.chain((pair) => O.fromNullable(pair[0].match(/ name="(\w+)"/))),
O.filter((nameMatch) => nameMatch.length > 0),
O.chain((nameMatch) =>
pipe(
nameMatch[0],
S.replace(/"/g, ""),
S.split("="),
O.fromPredicate((q) => q.length === 2),
O.map(
(nameArr) =>
[nameArr[1], pair[0].includes("filename") ? "" : pair[1]] as [
string,
string
]
)
)
)
)
},
}
const getFormDataBody = (rawData: string, rawContentType: string | undefined) =>
pipe(
multipartFunctions.getBoundary(rawData, rawContentType),
O.map((boundary) =>
pipe(
multipartFunctions.splitUsingBoundaryAndNewLines(rawData, boundary),
RA.filterMap((p) => multipartFunctions.getNameValuePair(p)),
RA.toArray
)
),
O.filter((arr) => arr.length > 0),
O.map(tupleToRecord)
)
const getHTMLBody = flow(formatHTML, O.of)
const getXMLBody = (rawData: string) =>
pipe(
rawData,
prettifyXml,
O.alt(() => O.some(rawData))
)
const getFormattedJSON = flow(
safeParseJSON,
O.map((parsedJSON) => JSON.stringify(parsedJSON, null, 2)),
O.getOrElse(() => "{}"),
O.of
)
const getXWWWFormUrlEncodedBody = flow(
decodeURIComponent,
(decoded) => decoded.match(/(([^&=]+)=?([^&=]*))/g),
O.fromNullable,
O.map((pairs) => pairs.map((p) => p.replace("=", ": ")).join("\n"))
)
/**
* Parses provided string according to the content type
* @param rawData Data to be parsed
* @param contentType Content type of the data
* @param rawContentType Optional parameter required for multipart/form-data
* @returns Option of parsed body as string or Record object for multipart/form-data
*/
export function parseBody(
rawData: string,
contentType: HoppRESTReqBody["contentType"],
rawContentType?: string
): O.Option<string | Record<string, string>> {
switch (contentType) {
case "application/hal+json":
case "application/ld+json":
case "application/vnd.api+json":
case "application/json":
return getFormattedJSON(rawData)
case "application/x-www-form-urlencoded":
return getXWWWFormUrlEncodedBody(rawData)
case "multipart/form-data":
return getFormDataBody(rawData, rawContentType)
case "text/html":
return getHTMLBody(rawData)
case "application/xml":
return getXMLBody(rawData)
case "text/plain":
default:
return O.some(rawData)
}
}
/**
* Formatter Functions
*/
/**
* Prettifies XML string
* @param sourceXml The string to format
* @returns Indented XML string (uses spaces)
*/
function prettifyXml(sourceXml: string) {
return pipe(
O.tryCatch(() => {
const xmlDoc = new DOMParser().parseFromString(
sourceXml,
"application/xml"
)
if (xmlDoc.querySelector("parsererror")) {
throw new Error("Unstructured Body")
}
const xsltDoc = new DOMParser().parseFromString(
[
// describes how we want to modify the XML - indent everything
'<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform">',
' <xsl:strip-space elements="*"/>',
' <xsl:template match="para[content-style][not(text())]">', // change to just text() to strip space in text nodes
' <xsl:value-of select="normalize-space(.)"/>',
" </xsl:template>",
' <xsl:template match="node()|@*">',
' <xsl:copy><xsl:apply-templates select="node()|@*"/></xsl:copy>',
" </xsl:template>",
' <xsl:output indent="yes"/>',
"</xsl:stylesheet>",
].join("\n"),
"application/xml"
)
const xsltProcessor = new XSLTProcessor()
xsltProcessor.importStylesheet(xsltDoc)
const resultDoc = xsltProcessor.transformToDocument(xmlDoc)
const resultXml = new XMLSerializer().serializeToString(resultDoc)
return resultXml
})
)
}
/**
* Prettifies HTML string
* @param htmlString The string to format
* @returns Indented HTML string (uses spaces)
*/
function formatHTML(htmlString: string) {
const tab = " "
let result = ""
let indent = ""
const emptyTags = [
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"param",
"source",
"track",
"wbr",
]
const spl = htmlString.split(/>\s*</)
spl.forEach((element) => {
if (element.match(/^\/\w/)) {
indent = indent.substring(tab.length)
}
result += indent + "<" + element + ">\n"
if (
element.match(/^<?\w[^>]*[^/]$/) &&
!emptyTags.includes(element.match(/^([a-z]*)/i)?.at(1) || "")
) {
indent += tab
}
})
return result.substring(1, result.length - 2)
}

View File

@@ -0,0 +1,27 @@
import parser from "yargs-parser"
import * as cookie from "cookie"
import * as O from "fp-ts/Option"
import * as S from "fp-ts/string"
import { pipe, flow } from "fp-ts/function"
import { objHasProperty } from "~/helpers/functional/object"
export function getCookies(parsedArguments: parser.Arguments) {
return pipe(
parsedArguments,
O.fromPredicate(objHasProperty("cookie", "string")),
O.map((args) => args.cookie),
O.alt(() =>
pipe(
parsedArguments,
O.fromPredicate(objHasProperty("b", "string")),
O.map((args) => args.b)
)
),
O.map(flow(S.replace(/^cookie: /i, ""), cookie.parse)),
O.getOrElse(() => ({}))
)
}

View File

@@ -0,0 +1,76 @@
import parser from "yargs-parser"
import { pipe, flow } from "fp-ts/function"
import { HoppRESTHeader } from "@hoppscotch/data"
import * as A from "fp-ts/Array"
import * as S from "fp-ts/string"
import * as O from "fp-ts/Option"
import { tupleToRecord } from "~/helpers/functional/record"
import {
objHasProperty,
objHasArrayProperty,
} from "~/helpers/functional/object"
const getHeaderPair = flow(
S.split(": "),
// must have a key and a value
O.fromPredicate((arr) => arr.length === 2),
O.map(([k, v]) => [k.trim(), v?.trim() ?? ""] as [string, string])
)
export function getHeaders(parsedArguments: parser.Arguments) {
let headers: Record<string, string> = {}
headers = pipe(
parsedArguments,
// make it an array if not already
O.fromPredicate(objHasProperty("H", "string")),
O.map((args) => [args.H]),
O.alt(() =>
pipe(
parsedArguments,
O.fromPredicate(objHasArrayProperty("H", "string")),
O.map((args) => args.H)
)
),
O.map(
flow(
A.map(getHeaderPair),
A.filterMap((a) => a),
tupleToRecord
)
),
O.getOrElseW(() => ({}))
)
if (
objHasProperty("A", "string")(parsedArguments) ||
objHasProperty("user-agent", "string")(parsedArguments)
)
headers["User-Agent"] = parsedArguments.A ?? parsedArguments["user-agent"]
const rawContentType =
headers["Content-Type"] ?? headers["content-type"] ?? ""
return {
headers,
rawContentType,
}
}
export const recordToHoppHeaders = (
headers: Record<string, string>
): HoppRESTHeader[] =>
pipe(
Object.keys(headers),
A.map((key) => ({
key,
value: headers[key],
active: true,
})),
A.filter(
(header) =>
header.key !== "Authorization" &&
header.key !== "content-type" &&
header.key !== "Content-Type"
)
)

View File

@@ -0,0 +1,68 @@
import parser from "yargs-parser"
import { pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as R from "fp-ts/Refinement"
import { getDefaultRESTRequest } from "~/newstore/RESTSession"
import {
objHasProperty,
objHasArrayProperty,
} from "~/helpers/functional/object"
const defaultRESTReq = getDefaultRESTRequest()
const getMethodFromXArg = (parsedArguments: parser.Arguments) =>
pipe(
parsedArguments,
O.fromPredicate(objHasProperty("X", "string")),
O.map((args) => args.X.trim()),
O.chain((xarg) =>
pipe(
O.fromNullable(
xarg.match(/GET|POST|PUT|PATCH|DELETE|HEAD|CONNECT|OPTIONS|TRACE/i)
),
O.alt(() => O.fromNullable(xarg.match(/[a-zA-Z]+/)))
)
),
O.map((method) => method[0])
)
const getMethodByDeduction = (parsedArguments: parser.Arguments) => {
if (
pipe(
objHasProperty("T", "string"),
R.or(objHasProperty("upload-file", "string"))
)(parsedArguments)
)
return O.some("put")
else if (
pipe(
objHasProperty("I", "boolean"),
R.or(objHasProperty("head", "boolean"))
)(parsedArguments)
)
return O.some("head")
else if (objHasProperty("G", "boolean")(parsedArguments)) return O.some("get")
else if (
pipe(
objHasProperty("d", "string"),
R.or(objHasArrayProperty("d", "string")),
R.or(objHasProperty("F", "string")),
R.or(objHasArrayProperty("F", "string"))
)(parsedArguments)
)
return O.some("post")
else return O.none
}
/**
* Get method type from X argument in curl string or
* find it out through other arguments
* @param parsedArguments Parsed Arguments object
* @returns Method string
*/
export const getMethod = (parsedArguments: parser.Arguments): string =>
pipe(
getMethodFromXArg(parsedArguments),
O.alt(() => getMethodByDeduction(parsedArguments)),
O.getOrElse(() => defaultRESTReq.method)
)

View File

@@ -0,0 +1,69 @@
import { pipe, flow } from "fp-ts/function"
import * as S from "fp-ts/string"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
const replaceables: { [key: string]: string } = {
"--request": "-X",
"--header": "-H",
"--url": "",
"--form": "-F",
"--data-raw": "--data",
"--data": "-d",
"--data-ascii": "-d",
"--data-binary": "-d",
"--user": "-u",
"--get": "-G",
}
const paperCuts = flow(
// remove '\' and newlines
S.replace(/ ?\\ ?$/gm, " "),
S.replace(/\n/g, ""),
// remove all $ symbols from start of argument values
S.replace(/\$'/g, "'"),
S.replace(/\$"/g, '"')
)
// replace --zargs option with -Z
const replaceLongOptions = (curlCmd: string) =>
pipe(Object.keys(replaceables), A.reduce(curlCmd, replaceFunction))
const replaceFunction = (curlCmd: string, r: string) =>
pipe(
curlCmd,
O.fromPredicate(
() => r.includes("data") || r.includes("form") || r.includes("header")
),
O.map(S.replace(RegExp(`[ \t]${r}(["' ])`, "g"), ` ${replaceables[r]}$1`)),
O.alt(() =>
pipe(
curlCmd,
S.replace(RegExp(`[ \t]${r}(["' ])`), ` ${replaceables[r]}$1`),
O.of
)
),
O.getOrElse(() => "")
)
// yargs parses -XPOST as separate arguments. just prescreen for it.
const prescreenXArgs = flow(
S.replace(
/ -X(GET|POST|PUT|PATCH|DELETE|HEAD|CONNECT|OPTIONS|TRACE)/,
" -X $1"
),
S.trim
)
/**
* Sanitizes and makes curl string processable
* @param curlCommand Raw curl command string
* @returns Processed curl command string
*/
export const preProcessCurlCommand = (curlCommand: string) =>
pipe(
curlCommand,
O.fromPredicate((curlCmd) => curlCmd.length > 0),
O.map(flow(paperCuts, replaceLongOptions, prescreenXArgs)),
O.getOrElse(() => "")
)

View File

@@ -0,0 +1,43 @@
import { pipe, flow } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import * as Sep from "fp-ts/Separated"
import { HoppRESTParam } from "@hoppscotch/data"
const isDangling = ([_, value]: [string, string]) => !value
/**
* Converts queries to HoppRESTParam format and separates dangling ones
* @param params Array of key value pairs of queries
* @returns Object containing separated queries and dangling queries
*/
export function getQueries(params: Array<[string, string]>): {
queries: Array<HoppRESTParam>
danglingParams: Array<string>
} {
return pipe(
params,
O.of,
O.map(
flow(
A.partition(isDangling),
Sep.bimap(
A.map(([key, value]) => ({
key,
value,
active: true,
})),
A.map(([key]) => key)
),
(sep) => ({
queries: sep.left,
danglingParams: sep.right,
})
)
),
O.getOrElseW(() => ({
queries: [],
danglingParams: [],
}))
)
}

View File

@@ -0,0 +1,80 @@
import parser from "yargs-parser"
import { pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import { getDefaultRESTRequest } from "~/newstore/RESTSession"
import { stringArrayJoin } from "~/helpers/functional/array"
const defaultRESTReq = getDefaultRESTRequest()
const getProtocolForBaseURL = (baseURL: string) =>
pipe(
// get the base URL
/^([^\s:@]+:[^\s:@]+@)?([^:/\s]+)([:]*)/.exec(baseURL),
O.fromNullable,
O.filter((burl) => burl.length > 1),
O.map((burl) => burl[2]),
// set protocol to http for local URLs
O.map((burl) =>
burl === "localhost" || burl === "127.0.0.1"
? "http://" + baseURL
: "https://" + baseURL
)
)
/**
* Processes URL string and returns the URL object
* @param parsedArguments Parsed Arguments object
* @returns URL object
*/
export function parseURL(parsedArguments: parser.Arguments) {
return pipe(
// contains raw url string
parsedArguments._[1],
O.fromNullable,
// preprocess url string
O.map((u) => u.toString().replace(/["']/g, "").trim()),
O.chain((u) =>
pipe(
// check if protocol is available
/^[^:\s]+(?=:\/\/)/.exec(u),
O.fromNullable,
O.map((_) => u),
O.alt(() => getProtocolForBaseURL(u))
)
),
O.map((u) => new URL(u)),
// no url found
O.getOrElse(() => new URL(defaultRESTReq.endpoint))
)
}
/**
* Joins dangling params to origin
* @param urlObject URL object containing origin and pathname
* @param danglingParams Keys of params with empty values
* @returns origin string concatenated with dangling paramas
*/
export function concatParams(urlObject: URL, danglingParams: string[]) {
return pipe(
O.Do,
O.bind("originString", () =>
pipe(
urlObject.origin,
O.fromPredicate((h) => h !== "")
)
),
O.map(({ originString }) =>
pipe(
danglingParams,
O.fromPredicate((dp) => dp.length > 0),
O.map(stringArrayJoin("&")),
O.map((h) => originString + (urlObject.pathname || "") + "?" + h),
O.getOrElse(() => originString + (urlObject.pathname || ""))
)
),
O.getOrElse(() => defaultRESTReq.endpoint)
)
}