refactor: move rawKeyValue and templating to hopp/data + rewrite rawKeyValue parsing
This commit is contained in:
94
packages/hoppscotch-data/src/environment.ts
Normal file
94
packages/hoppscotch-data/src/environment.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
export type Environment = {
|
||||
name: string
|
||||
variables: {
|
||||
key: string
|
||||
value: string
|
||||
}[]
|
||||
}
|
||||
|
||||
const REGEX_ENV_VAR = /<<([^>]*)>>/g // "<<myVariable>>"
|
||||
|
||||
/**
|
||||
* How much times can we expand environment variables
|
||||
*/
|
||||
const ENV_MAX_EXPAND_LIMIT = 10
|
||||
|
||||
/**
|
||||
* Error state when there is a suspected loop while
|
||||
* recursively expanding variables
|
||||
*/
|
||||
const ENV_EXPAND_LOOP = "ENV_EXPAND_LOOP" as const
|
||||
|
||||
export function parseBodyEnvVariablesE(
|
||||
body: string,
|
||||
env: Environment["variables"]
|
||||
) {
|
||||
let result = body
|
||||
let depth = 0
|
||||
|
||||
while (result.match(REGEX_ENV_VAR) != null && depth <= ENV_MAX_EXPAND_LIMIT) {
|
||||
result = result.replace(/<<\w+>>/g, (key) => {
|
||||
const found = env.find(
|
||||
(envVar) => envVar.key === key.replace(/[<>]/g, "")
|
||||
)
|
||||
return found ? found.value : key
|
||||
})
|
||||
|
||||
depth++
|
||||
}
|
||||
|
||||
return depth > ENV_MAX_EXPAND_LIMIT
|
||||
? E.left(ENV_EXPAND_LOOP)
|
||||
: E.right(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `parseBodyEnvVariablesE` instead.
|
||||
*/
|
||||
export const parseBodyEnvVariables = (
|
||||
body: string,
|
||||
env: Environment["variables"]
|
||||
) =>
|
||||
pipe(
|
||||
parseBodyEnvVariablesE(body, env),
|
||||
E.getOrElse(() => body)
|
||||
)
|
||||
|
||||
export function parseTemplateStringE(
|
||||
str: string,
|
||||
variables: Environment["variables"]
|
||||
) {
|
||||
if (!variables || !str) {
|
||||
return E.right(str)
|
||||
}
|
||||
|
||||
let result = str
|
||||
let depth = 0
|
||||
|
||||
while (result.match(REGEX_ENV_VAR) != null && depth <= ENV_MAX_EXPAND_LIMIT) {
|
||||
result = decodeURI(encodeURI(result)).replace(
|
||||
REGEX_ENV_VAR,
|
||||
(_, p1) => variables.find((x) => x.key === p1)?.value || ""
|
||||
)
|
||||
depth++
|
||||
}
|
||||
|
||||
return depth > ENV_MAX_EXPAND_LIMIT
|
||||
? E.left(ENV_EXPAND_LOOP)
|
||||
: E.right(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `parseTemplateStringE` instead
|
||||
*/
|
||||
export const parseTemplateString = (
|
||||
str: string,
|
||||
variables: Environment["variables"]
|
||||
) =>
|
||||
pipe(
|
||||
parseTemplateStringE(str, variables),
|
||||
E.getOrElse(() => str)
|
||||
)
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from "./rest"
|
||||
export * from "./graphql"
|
||||
export * from "./collection"
|
||||
export * from "./rawKeyValue"
|
||||
export * from "./environment"
|
||||
|
||||
178
packages/hoppscotch-data/src/rawKeyValue.ts
Normal file
178
packages/hoppscotch-data/src/rawKeyValue.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { not } from "fp-ts/Predicate"
|
||||
import { pipe, flow } from "fp-ts/function"
|
||||
import * as Str from "fp-ts/string"
|
||||
import * as RA from "fp-ts/ReadonlyArray"
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as P from "parser-ts/Parser"
|
||||
import * as S from "parser-ts/string"
|
||||
import * as C from "parser-ts/char"
|
||||
|
||||
export type RawKeyValueEntry = {
|
||||
key: string
|
||||
value: string
|
||||
active: boolean
|
||||
}
|
||||
|
||||
/* Beginning of Parser Definitions */
|
||||
|
||||
const wsSurround = P.surroundedBy(S.spaces)
|
||||
|
||||
const stringArrayJoin = (sep: string) => (input: string[]) => input.join(sep)
|
||||
|
||||
const stringTakeUntilChars = (chars: C.Char[]) => pipe(
|
||||
P.takeUntil((c: C.Char) => chars.includes(c)),
|
||||
P.map(stringArrayJoin("")),
|
||||
)
|
||||
|
||||
const stringTakeUntilCharsInclusive = flow(
|
||||
stringTakeUntilChars,
|
||||
P.chainFirst(() => P.sat(() => true)),
|
||||
)
|
||||
|
||||
const key = pipe(
|
||||
stringTakeUntilChars([":", "\n"]),
|
||||
P.map(Str.trim)
|
||||
)
|
||||
|
||||
const value = pipe(
|
||||
stringTakeUntilChars(["\n"]),
|
||||
P.map(Str.trim)
|
||||
)
|
||||
|
||||
const commented = pipe(
|
||||
S.maybe(S.string("#")),
|
||||
P.map(not(Str.isEmpty))
|
||||
)
|
||||
|
||||
const line = pipe(
|
||||
wsSurround(commented),
|
||||
P.bindTo("commented"),
|
||||
|
||||
P.bind("key", () => wsSurround(key)),
|
||||
|
||||
P.chainFirst(() => C.char(":")),
|
||||
|
||||
P.bind("value", () => value),
|
||||
)
|
||||
|
||||
const lineWithNoColon = pipe(
|
||||
wsSurround(commented),
|
||||
P.bindTo("commented"),
|
||||
P.bind("key", () => stringTakeUntilCharsInclusive(["\n"])),
|
||||
P.map(flow(
|
||||
O.fromPredicate(({ key }) => !Str.isEmpty(key))
|
||||
))
|
||||
)
|
||||
|
||||
const file = pipe(
|
||||
P.manyTill(line, P.eof()),
|
||||
)
|
||||
|
||||
/**
|
||||
* This Raw Key Value parser ignores the key value pair (no colon) issues
|
||||
*/
|
||||
const tolerantFile = pipe(
|
||||
P.manyTill(
|
||||
P.either(
|
||||
pipe(line, P.map(O.some)),
|
||||
() => pipe(
|
||||
lineWithNoColon,
|
||||
P.map(flow(
|
||||
O.map((a) => ({ ...a, value: "" }))
|
||||
))
|
||||
)
|
||||
),
|
||||
P.eof()
|
||||
),
|
||||
P.map(flow(
|
||||
RA.filterMap(flow(
|
||||
O.fromPredicate(O.isSome),
|
||||
O.map((a) => a.value)
|
||||
))
|
||||
))
|
||||
)
|
||||
|
||||
/* End of Parser Definitions */
|
||||
|
||||
/**
|
||||
* Converts Raw Key Value Entries to the file string format
|
||||
* @param entries The entries array
|
||||
* @returns The entries in string format
|
||||
*/
|
||||
export const rawKeyValueEntriesToString = (entries: RawKeyValueEntry[]) =>
|
||||
pipe(
|
||||
entries,
|
||||
A.map(({ key, value, active }) =>
|
||||
active ? `${key}: ${value}` : `# ${key}: ${value}`
|
||||
),
|
||||
stringArrayJoin("\n")
|
||||
)
|
||||
|
||||
/**
|
||||
* Parses raw key value entries string to array
|
||||
* @param s The file string to parse from
|
||||
* @returns Either the parser fail result or the raw key value entries
|
||||
*/
|
||||
export const parseRawKeyValueEntriesE = (s: string) =>
|
||||
pipe(
|
||||
tolerantFile,
|
||||
S.run(s),
|
||||
E.mapLeft((err) => ({
|
||||
message: `Expected ${err.expected.map((x) => `'${x}'`).join(", ")}`,
|
||||
expected: err.expected,
|
||||
pos: err.input.cursor,
|
||||
})),
|
||||
E.map(
|
||||
({ value }) => pipe(
|
||||
value,
|
||||
RA.map(({ key, value, commented }) =>
|
||||
<RawKeyValueEntry>{
|
||||
active: !commented,
|
||||
key,
|
||||
value
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* Less error tolerating version of `parseRawKeyValueEntriesE`
|
||||
* @param s The file string to parse from
|
||||
* @returns Either the parser fail result or the raw key value entries
|
||||
*/
|
||||
export const strictParseRawKeyValueEntriesE = (s: string) =>
|
||||
pipe(
|
||||
file,
|
||||
S.run(s),
|
||||
E.mapLeft((err) => ({
|
||||
message: `Expected ${err.expected.map((x) => `'${x}'`).join(", ")}`,
|
||||
expected: err.expected,
|
||||
pos: err.input.cursor,
|
||||
})),
|
||||
E.map(
|
||||
({ value }) => pipe(
|
||||
value,
|
||||
RA.map(({ key, value, commented }) =>
|
||||
<RawKeyValueEntry>{
|
||||
active: !commented,
|
||||
key,
|
||||
value
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* Kept for legacy code compatibility, parses raw key value entries.
|
||||
* If failed, it returns an empty array
|
||||
* @deprecated Use `parseRawKeyValueEntriesE` instead
|
||||
*/
|
||||
export const parseRawKeyValueEntries = flow(
|
||||
parseRawKeyValueEntriesE,
|
||||
E.map(RA.toArray),
|
||||
E.getOrElse(() => [] as RawKeyValueEntry[])
|
||||
)
|
||||
Reference in New Issue
Block a user