BodyTransition.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. /**
  2. * Defines how body should be updated for movement between different
  3. * content-types
  4. */
  5. import { pipe } from "fp-ts/function"
  6. import * as A from "fp-ts/Array"
  7. import {
  8. FormDataKeyValue,
  9. HoppRESTReqBody,
  10. ValidContentTypes,
  11. parseRawKeyValueEntries,
  12. rawKeyValueEntriesToString,
  13. RawKeyValueEntry,
  14. } from "@hoppscotch/data"
  15. const ANY_TYPE = Symbol("TRANSITION_RULESET_IGNORE_TYPE")
  16. // eslint-disable-next-line no-redeclare
  17. type ANY_TYPE = typeof ANY_TYPE
  18. type BodyType<T extends ValidContentTypes | null | ANY_TYPE> =
  19. T extends ValidContentTypes
  20. ? HoppRESTReqBody & { contentType: T }
  21. : HoppRESTReqBody
  22. type TransitionDefinition<
  23. FromType extends ValidContentTypes | null | ANY_TYPE,
  24. ToType extends ValidContentTypes | null | ANY_TYPE
  25. > = {
  26. from: FromType
  27. to: ToType
  28. definition: (
  29. currentBody: BodyType<FromType>,
  30. targetType: BodyType<ToType>["contentType"]
  31. ) => BodyType<ToType>
  32. }
  33. const rule = <
  34. FromType extends ValidContentTypes | null | ANY_TYPE,
  35. ToType extends ValidContentTypes | null | ANY_TYPE
  36. >(
  37. input: TransitionDefinition<FromType, ToType>
  38. ) => input
  39. // Use ANY_TYPE to ignore from/dest type
  40. // Rules apply from top to bottom
  41. const transitionRuleset = [
  42. rule({
  43. from: null,
  44. to: "multipart/form-data",
  45. definition: () => ({
  46. contentType: "multipart/form-data",
  47. body: [],
  48. }),
  49. }),
  50. rule({
  51. from: ANY_TYPE,
  52. to: null,
  53. definition: () => ({
  54. contentType: null,
  55. body: null,
  56. }),
  57. }),
  58. rule({
  59. from: null,
  60. to: ANY_TYPE,
  61. definition: (_, targetType) => ({
  62. contentType: targetType as unknown as Exclude<
  63. // This is guaranteed by the above rules, we just can't tell TS this
  64. ValidContentTypes,
  65. "multipart/form-data"
  66. >,
  67. body: "",
  68. }),
  69. }),
  70. rule({
  71. from: "multipart/form-data",
  72. to: "application/x-www-form-urlencoded",
  73. definition: (currentBody, targetType) => ({
  74. contentType: targetType,
  75. body: pipe(
  76. currentBody.body,
  77. A.map(
  78. ({ key, value, isFile, active }) =>
  79. <RawKeyValueEntry>{
  80. key,
  81. value: isFile ? "" : value,
  82. active,
  83. }
  84. ),
  85. rawKeyValueEntriesToString
  86. ),
  87. }),
  88. }),
  89. rule({
  90. from: "application/x-www-form-urlencoded",
  91. to: "multipart/form-data",
  92. definition: (currentBody, targetType) => ({
  93. contentType: targetType,
  94. body: pipe(
  95. currentBody.body,
  96. parseRawKeyValueEntries,
  97. A.map(
  98. ({ key, value, active }) =>
  99. <FormDataKeyValue>{
  100. key,
  101. value,
  102. active,
  103. isFile: false,
  104. }
  105. )
  106. ),
  107. }),
  108. }),
  109. rule({
  110. from: ANY_TYPE,
  111. to: "multipart/form-data",
  112. definition: () => ({
  113. contentType: "multipart/form-data",
  114. body: [],
  115. }),
  116. }),
  117. rule({
  118. from: "multipart/form-data",
  119. to: ANY_TYPE,
  120. definition: (_, target) => ({
  121. contentType: target as Exclude<ValidContentTypes, "multipart/form-data">,
  122. body: "",
  123. }),
  124. }),
  125. rule({
  126. from: "application/x-www-form-urlencoded",
  127. to: ANY_TYPE,
  128. definition: (_, target) => ({
  129. contentType: target as Exclude<ValidContentTypes, "multipart/form-data">,
  130. body: "",
  131. }),
  132. }),
  133. rule({
  134. from: ANY_TYPE,
  135. to: "application/x-www-form-urlencoded",
  136. definition: () => ({
  137. contentType: "application/x-www-form-urlencoded",
  138. body: "",
  139. }),
  140. }),
  141. rule({
  142. from: ANY_TYPE,
  143. to: ANY_TYPE,
  144. definition: (curr, targetType) => ({
  145. contentType: targetType as Exclude<
  146. // Above rules ensure this will be the case
  147. ValidContentTypes,
  148. "multipart/form-data" | "application/x-www-form-urlencoded"
  149. >,
  150. // Again, above rules ensure this will be the case, can't convince TS tho
  151. body: (
  152. curr as HoppRESTReqBody & {
  153. contentType: Exclude<
  154. ValidContentTypes,
  155. "multipart/form-data" | "application/x-www-form-urlencoded"
  156. >
  157. }
  158. ).body,
  159. }),
  160. }),
  161. ] as const
  162. export const applyBodyTransition = <T extends ValidContentTypes | null>(
  163. current: HoppRESTReqBody,
  164. target: T
  165. ): HoppRESTReqBody & { contentType: T } => {
  166. if (current.contentType === target) {
  167. console.warn(
  168. `Tried to transition body from and to the same content-type '${target}'`
  169. )
  170. return current as any
  171. }
  172. const transitioner = transitionRuleset.find(
  173. (def) =>
  174. (def.from === current.contentType || def.from === ANY_TYPE) &&
  175. (def.to === target || def.to === ANY_TYPE)
  176. )
  177. if (!transitioner) {
  178. throw new Error("Body Type Transition Ruleset is invalid :(")
  179. }
  180. // TypeScript won't be able to figure this out easily :(
  181. return (transitioner.definition as any)(current, target)
  182. }