refactor: body type transition to ruleset based system
This commit is contained in:
196
packages/hoppscotch-app/helpers/rules/BodyTransition.ts
Normal file
196
packages/hoppscotch-app/helpers/rules/BodyTransition.ts
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
/**
|
||||||
|
* Defines how body should be updated for movement between different
|
||||||
|
* content-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { pipe } from "fp-ts/function"
|
||||||
|
import * as A from "fp-ts/Array"
|
||||||
|
import {
|
||||||
|
FormDataKeyValue,
|
||||||
|
HoppRESTReqBody,
|
||||||
|
ValidContentTypes,
|
||||||
|
} from "@hoppscotch/data"
|
||||||
|
import {
|
||||||
|
parseRawKeyValueEntries,
|
||||||
|
rawKeyValueEntriesToString,
|
||||||
|
RawKeyValueEntry,
|
||||||
|
} from "../rawKeyValue"
|
||||||
|
|
||||||
|
const ANY_TYPE = Symbol("TRANSITION_RULESET_IGNORE_TYPE")
|
||||||
|
// eslint-disable-next-line no-redeclare
|
||||||
|
type ANY_TYPE = typeof ANY_TYPE
|
||||||
|
|
||||||
|
type BodyType<T extends ValidContentTypes | null | ANY_TYPE> =
|
||||||
|
T extends ValidContentTypes
|
||||||
|
? HoppRESTReqBody & { contentType: T }
|
||||||
|
: HoppRESTReqBody
|
||||||
|
|
||||||
|
type TransitionDefinition<
|
||||||
|
FromType extends ValidContentTypes | null | ANY_TYPE,
|
||||||
|
ToType extends ValidContentTypes | null | ANY_TYPE
|
||||||
|
> = {
|
||||||
|
from: FromType
|
||||||
|
to: ToType
|
||||||
|
definition: (
|
||||||
|
currentBody: BodyType<FromType>,
|
||||||
|
targetType: BodyType<ToType>["contentType"]
|
||||||
|
) => BodyType<ToType>
|
||||||
|
}
|
||||||
|
|
||||||
|
const rule = <
|
||||||
|
FromType extends ValidContentTypes | null | ANY_TYPE,
|
||||||
|
ToType extends ValidContentTypes | null | ANY_TYPE
|
||||||
|
>(
|
||||||
|
input: TransitionDefinition<FromType, ToType>
|
||||||
|
) => input
|
||||||
|
|
||||||
|
// Use ANY_TYPE to ignore from/dest type
|
||||||
|
// Rules apply from top to bottom
|
||||||
|
const transitionRuleset = [
|
||||||
|
rule({
|
||||||
|
from: null,
|
||||||
|
to: "multipart/form-data",
|
||||||
|
definition: () => ({
|
||||||
|
contentType: "multipart/form-data",
|
||||||
|
body: [],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
rule({
|
||||||
|
from: ANY_TYPE,
|
||||||
|
to: null,
|
||||||
|
definition: () => ({
|
||||||
|
contentType: null,
|
||||||
|
body: null,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
rule({
|
||||||
|
from: null,
|
||||||
|
to: ANY_TYPE,
|
||||||
|
definition: (_, targetType) => ({
|
||||||
|
contentType: targetType as unknown as Exclude<
|
||||||
|
// This is guaranteed by the above rules, we just can't tell TS this
|
||||||
|
ValidContentTypes,
|
||||||
|
"multipart/form-data"
|
||||||
|
>,
|
||||||
|
body: "",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
rule({
|
||||||
|
from: "multipart/form-data",
|
||||||
|
to: "application/x-www-form-urlencoded",
|
||||||
|
definition: (currentBody, targetType) => ({
|
||||||
|
contentType: targetType,
|
||||||
|
body: pipe(
|
||||||
|
currentBody.body,
|
||||||
|
A.map(
|
||||||
|
({ key, value, isFile, active }) =>
|
||||||
|
<RawKeyValueEntry>{
|
||||||
|
key,
|
||||||
|
value: isFile ? "" : value,
|
||||||
|
active,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
rawKeyValueEntriesToString
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
rule({
|
||||||
|
from: "application/x-www-form-urlencoded",
|
||||||
|
to: "multipart/form-data",
|
||||||
|
definition: (currentBody, targetType) => ({
|
||||||
|
contentType: targetType,
|
||||||
|
body: pipe(
|
||||||
|
currentBody.body,
|
||||||
|
parseRawKeyValueEntries,
|
||||||
|
A.map(
|
||||||
|
({ key, value, active }) =>
|
||||||
|
<FormDataKeyValue>{
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
active,
|
||||||
|
isFile: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
rule({
|
||||||
|
from: ANY_TYPE,
|
||||||
|
to: "multipart/form-data",
|
||||||
|
definition: () => ({
|
||||||
|
contentType: "multipart/form-data",
|
||||||
|
body: [],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
rule({
|
||||||
|
from: "multipart/form-data",
|
||||||
|
to: ANY_TYPE,
|
||||||
|
definition: (_, target) => ({
|
||||||
|
contentType: target as Exclude<ValidContentTypes, "multipart/form-data">,
|
||||||
|
body: "",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
rule({
|
||||||
|
from: "application/x-www-form-urlencoded",
|
||||||
|
to: ANY_TYPE,
|
||||||
|
definition: (_, target) => ({
|
||||||
|
contentType: target as Exclude<ValidContentTypes, "multipart/form-data">,
|
||||||
|
body: "",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
rule({
|
||||||
|
from: ANY_TYPE,
|
||||||
|
to: "application/x-www-form-urlencoded",
|
||||||
|
definition: () => ({
|
||||||
|
contentType: "application/x-www-form-urlencoded",
|
||||||
|
body: "",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
rule({
|
||||||
|
from: ANY_TYPE,
|
||||||
|
to: ANY_TYPE,
|
||||||
|
definition: (curr, targetType) => ({
|
||||||
|
contentType: targetType as Exclude<
|
||||||
|
// Above rules ensure this will be the case
|
||||||
|
ValidContentTypes,
|
||||||
|
"multipart/form-data" | "application/x-www-form-urlencoded"
|
||||||
|
>,
|
||||||
|
// Again, above rules ensure this will be the case, can't convince TS tho
|
||||||
|
body: (
|
||||||
|
curr as HoppRESTReqBody & {
|
||||||
|
contentType: Exclude<
|
||||||
|
ValidContentTypes,
|
||||||
|
"multipart/form-data" | "application/x-www-form-urlencoded"
|
||||||
|
>
|
||||||
|
}
|
||||||
|
).body,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const applyBodyTransition = <T extends ValidContentTypes | null>(
|
||||||
|
current: HoppRESTReqBody,
|
||||||
|
target: T
|
||||||
|
): HoppRESTReqBody & { contentType: T } => {
|
||||||
|
debugger
|
||||||
|
|
||||||
|
if (current.contentType === target) {
|
||||||
|
console.warn(
|
||||||
|
`Tried to transition body from and to the same content-type '${target}'`
|
||||||
|
)
|
||||||
|
return current as any
|
||||||
|
}
|
||||||
|
|
||||||
|
const transitioner = transitionRuleset.find(
|
||||||
|
(def) =>
|
||||||
|
(def.from === current.contentType || def.from === ANY_TYPE) &&
|
||||||
|
(def.to === target || def.to === ANY_TYPE)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!transitioner) {
|
||||||
|
throw new Error("Body Type Transition Ruleset is invalid :(")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeScript won't be able to figure this out easily :(
|
||||||
|
return (transitioner.definition as any)(current, target)
|
||||||
|
}
|
||||||
@@ -12,6 +12,9 @@ export const objectFieldIncludes = <
|
|||||||
field: K,
|
field: K,
|
||||||
values: V
|
values: V
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
): obj is T & { [_x in K]: V[number] } => {
|
): obj is T & { [_x in K]: V[number] } => values.includes(obj[field])
|
||||||
return values.includes(obj[field])
|
|
||||||
}
|
export const valueIncludes = <T, V extends readonly T[]>(
|
||||||
|
obj: T,
|
||||||
|
values: V
|
||||||
|
): obj is V[number] => values.includes(obj)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import * as A from "fp-ts/Array"
|
|
||||||
import { pipe } from "fp-ts/function"
|
|
||||||
import { pluck, distinctUntilChanged, map, filter } from "rxjs/operators"
|
import { pluck, distinctUntilChanged, map, filter } from "rxjs/operators"
|
||||||
import { Ref } from "@nuxtjs/composition-api"
|
import { Ref } from "@nuxtjs/composition-api"
|
||||||
import {
|
import {
|
||||||
@@ -17,11 +15,7 @@ import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
|||||||
import { useStream } from "~/helpers/utils/composables"
|
import { useStream } from "~/helpers/utils/composables"
|
||||||
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
|
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
|
||||||
import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
|
import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
|
||||||
import {
|
import { applyBodyTransition } from "~/helpers/rules/BodyTransition"
|
||||||
parseRawKeyValueEntries,
|
|
||||||
rawKeyValueEntriesToString,
|
|
||||||
RawKeyValueEntry,
|
|
||||||
} from "~/helpers/rawKeyValue"
|
|
||||||
|
|
||||||
type RESTSession = {
|
type RESTSession = {
|
||||||
request: HoppRESTRequest
|
request: HoppRESTRequest
|
||||||
@@ -212,101 +206,107 @@ const dispatchers = defineDispatchers({
|
|||||||
) {
|
) {
|
||||||
// TODO: Cleaner implementation
|
// TODO: Cleaner implementation
|
||||||
// TODO: persist body evenafter switching content typees
|
// TODO: persist body evenafter switching content typees
|
||||||
if (curr.request.body.contentType !== "multipart/form-data") {
|
return {
|
||||||
if (newContentType === "multipart/form-data") {
|
request: {
|
||||||
// Preserve entries when comping from urlencoded to multipart
|
...curr.request,
|
||||||
if (
|
body: applyBodyTransition(curr.request.body, newContentType),
|
||||||
curr.request.body.contentType === "application/x-www-form-urlencoded"
|
},
|
||||||
) {
|
|
||||||
return {
|
|
||||||
request: {
|
|
||||||
...curr.request,
|
|
||||||
body: <HoppRESTReqBody>{
|
|
||||||
contentType: "multipart/form-data",
|
|
||||||
body: pipe(
|
|
||||||
curr.request.body.body,
|
|
||||||
parseRawKeyValueEntries,
|
|
||||||
A.map(
|
|
||||||
({ key, value, active }) =>
|
|
||||||
<FormDataKeyValue>{ key, value, active, isFile: false }
|
|
||||||
)
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Going from non-formdata to form-data, discard contents and set empty array as body
|
|
||||||
return {
|
|
||||||
request: {
|
|
||||||
...curr.request,
|
|
||||||
body: <HoppRESTReqBody>{
|
|
||||||
contentType: "multipart/form-data",
|
|
||||||
body: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// non-formdata to non-formdata, keep body and set content type
|
|
||||||
return {
|
|
||||||
request: {
|
|
||||||
...curr.request,
|
|
||||||
body: <HoppRESTReqBody>{
|
|
||||||
contentType: newContentType,
|
|
||||||
body:
|
|
||||||
newContentType === null
|
|
||||||
? null
|
|
||||||
: (curr.request.body as any)?.body ?? "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (newContentType !== "multipart/form-data") {
|
|
||||||
if (newContentType === "application/x-www-form-urlencoded") {
|
|
||||||
return {
|
|
||||||
request: {
|
|
||||||
...curr.request,
|
|
||||||
body: <HoppRESTReqBody>{
|
|
||||||
contentType: newContentType,
|
|
||||||
body: pipe(
|
|
||||||
curr.request.body.body,
|
|
||||||
A.map(
|
|
||||||
({ key, value, isFile, active }) =>
|
|
||||||
<RawKeyValueEntry>{
|
|
||||||
key,
|
|
||||||
value: isFile ? "" : value,
|
|
||||||
active,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
rawKeyValueEntriesToString
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Going from formdata to non-formdata, discard contents and set empty string
|
|
||||||
return {
|
|
||||||
request: {
|
|
||||||
...curr.request,
|
|
||||||
body: <HoppRESTReqBody>{
|
|
||||||
contentType: newContentType,
|
|
||||||
body: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// form-data to form-data ? just set the content type ¯\_(ツ)_/¯
|
|
||||||
return {
|
|
||||||
request: {
|
|
||||||
...curr.request,
|
|
||||||
body: <HoppRESTReqBody>{
|
|
||||||
contentType: newContentType,
|
|
||||||
body: curr.request.body.body,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// if (curr.request.body.contentType !== "multipart/form-data") {
|
||||||
|
// if (newContentType === "multipart/form-data") {
|
||||||
|
// // Preserve entries when comping from urlencoded to multipart
|
||||||
|
// if (
|
||||||
|
// curr.request.body.contentType === "application/x-www-form-urlencoded"
|
||||||
|
// ) {
|
||||||
|
// return {
|
||||||
|
// request: {
|
||||||
|
// ...curr.request,
|
||||||
|
// body: <HoppRESTReqBody>{
|
||||||
|
// contentType: "multipart/form-data",
|
||||||
|
// body: pipe(
|
||||||
|
// curr.request.body.body,
|
||||||
|
// parseRawKeyValueEntries,
|
||||||
|
// A.map(
|
||||||
|
// ({ key, value, active }) =>
|
||||||
|
// <FormDataKeyValue>{ key, value, active, isFile: false }
|
||||||
|
// )
|
||||||
|
// ),
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Going from non-formdata to form-data, discard contents and set empty array as body
|
||||||
|
// return {
|
||||||
|
// request: {
|
||||||
|
// ...curr.request,
|
||||||
|
// body: <HoppRESTReqBody>{
|
||||||
|
// contentType: "multipart/form-data",
|
||||||
|
// body: [],
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// // non-formdata to non-formdata, keep body and set content type
|
||||||
|
// return {
|
||||||
|
// request: {
|
||||||
|
// ...curr.request,
|
||||||
|
// body: <HoppRESTReqBody>{
|
||||||
|
// contentType: newContentType,
|
||||||
|
// body:
|
||||||
|
// newContentType === null
|
||||||
|
// ? null
|
||||||
|
// : (curr.request.body as any)?.body ?? "",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } else if (newContentType !== "multipart/form-data") {
|
||||||
|
// if (newContentType === "application/x-www-form-urlencoded") {
|
||||||
|
// return {
|
||||||
|
// request: {
|
||||||
|
// ...curr.request,
|
||||||
|
// body: <HoppRESTReqBody>{
|
||||||
|
// contentType: newContentType,
|
||||||
|
// body: pipe(
|
||||||
|
// curr.request.body.body,
|
||||||
|
// A.map(
|
||||||
|
// ({ key, value, isFile, active }) =>
|
||||||
|
// <RawKeyValueEntry>{
|
||||||
|
// key,
|
||||||
|
// value: isFile ? "" : value,
|
||||||
|
// active,
|
||||||
|
// }
|
||||||
|
// ),
|
||||||
|
// rawKeyValueEntriesToString
|
||||||
|
// ),
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Going from formdata to non-formdata, discard contents and set empty string
|
||||||
|
// return {
|
||||||
|
// request: {
|
||||||
|
// ...curr.request,
|
||||||
|
// body: <HoppRESTReqBody>{
|
||||||
|
// contentType: newContentType,
|
||||||
|
// body: "",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// // form-data to form-data ? just set the content type ¯\_(ツ)_/¯
|
||||||
|
// return {
|
||||||
|
// request: {
|
||||||
|
// ...curr.request,
|
||||||
|
// body: <HoppRESTReqBody>{
|
||||||
|
// contentType: newContentType,
|
||||||
|
// body: curr.request.body.body,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// }
|
||||||
},
|
},
|
||||||
addFormDataEntry(curr: RESTSession, { entry }: { entry: FormDataKeyValue }) {
|
addFormDataEntry(curr: RESTSession, { entry }: { entry: FormDataKeyValue }) {
|
||||||
// Only perform update if the current content-type is formdata
|
// Only perform update if the current content-type is formdata
|
||||||
|
|||||||
Reference in New Issue
Block a user