refactor: move rawKeyValue and templating to hopp/data + rewrite rawKeyValue parsing

This commit is contained in:
Andrew Bastin
2022-02-15 23:46:01 +05:30
parent cca819b125
commit c3759a400d
17 changed files with 266 additions and 121 deletions

View 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)
)

View File

@@ -1,3 +1,5 @@
export * from "./rest"
export * from "./graphql"
export * from "./collection"
export * from "./rawKeyValue"
export * from "./environment"

View 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[])
)