added files to start parsing functionality of path variables

This commit is contained in:
isaiM6
2022-07-29 18:01:24 -07:00
6 changed files with 292 additions and 9 deletions

View File

@@ -35,10 +35,13 @@ import { EditorState, Extension } from "@codemirror/state"
import clone from "lodash/clone"
import { tooltips } from "@codemirror/tooltip"
import { history, historyKeymap } from "@codemirror/history"
import { HoppRESTVar } from "@hoppscotch/data"
import { inputTheme } from "~/helpers/editor/themes/baseTheme"
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
import { useReadonlyStream } from "~/helpers/utils/composables"
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
import { HoppReactiveVarPlugin } from "~/helpers/editor/extensions/HoppVariable"
import { restVars$ } from "~/newstore/RESTSession"
const props = withDefaults(
defineProps<{
@@ -46,6 +49,7 @@ const props = withDefaults(
placeholder: string
styles: string
envs: { key: string; value: string; source: string }[] | null
vars: { key: string; value: string }[] | null
focus: boolean
readonly: boolean
}>(),
@@ -54,6 +58,7 @@ const props = withDefaults(
placeholder: "",
styles: "",
envs: null,
vars: null,
focus: false,
readonly: false,
}
@@ -109,6 +114,7 @@ let pastedValue: string | null = null
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, []) as Ref<
AggregateEnvironment[]
>
const aggregateVars = useReadonlyStream(restVars$, []) as Ref<HoppRESTVar[]>
const envVars = computed(() =>
props.envs
@@ -120,7 +126,17 @@ const envVars = computed(() =>
: aggregateEnvs.value
)
const varVars = computed(() =>
props.vars
? props.vars.map((x) => ({
key: x.key,
value: x.value,
}))
: aggregateVars.value
)
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
const varTooltipPlugin = new HoppReactiveVarPlugin(varVars, view)
const initView = (el: any) => {
const extensions: Extension = [
@@ -146,6 +162,7 @@ const initView = (el: any) => {
position: "absolute",
}),
envTooltipPlugin,
varTooltipPlugin,
placeholderExt(props.placeholder),
EditorView.domEventHandlers({
paste(ev) {

View File

@@ -0,0 +1,180 @@
import { watch, Ref } from "@nuxtjs/composition-api"
import { Compartment } from "@codemirror/state"
import { hoverTooltip } from "@codemirror/tooltip"
import {
Decoration,
EditorView,
MatchDecorator,
ViewPlugin,
} from "@codemirror/view"
import * as E from "fp-ts/Either"
import { HoppRESTVar, parseTemplateStringE } from "@hoppscotch/data"
const HOPP_ENVIRONMENT_REGEX = /({{\w+}})/g
const HOPP_ENV_HIGHLIGHT =
"cursor-help transition rounded px-1 focus:outline-none mx-0.5 env-highlight"
const HOPP_ENV_HIGHLIGHT_FOUND =
"bg-accentDark text-accentContrast hover:bg-accent"
const HOPP_ENV_HIGHLIGHT_NOT_FOUND =
"bg-red-500 text-accentContrast hover:bg-red-600"
const cursorTooltipField = (aggregateEnvs: HoppRESTVar[]) =>
hoverTooltip(
(view, pos, side) => {
const { from, to, text } = view.state.doc.lineAt(pos)
// TODO: When Codemirror 6 allows this to work (not make the
// popups appear half of the time) use this implementation
// const wordSelection = view.state.wordAt(pos)
// if (!wordSelection) return null
// const word = view.state.doc.sliceString(
// wordSelection.from - 2,
// wordSelection.to + 2
// )
// if (!HOPP_ENVIRONMENT_REGEX.test(word)) return null
// Tracking the start and the end of the words
let start = pos
let end = pos
while (start > from && /\w/.test(text[start - from - 1])) start--
while (end < to && /\w/.test(text[end - from])) end++
if (
(start === pos && side < 0) ||
(end === pos && side > 0) ||
!HOPP_ENVIRONMENT_REGEX.test(
text.slice(start - from - 2, end - from + 2)
)
)
return null
const envValue =
aggregateEnvs.find(
(env) => env.key === text.slice(start - from, end - from)
// env.key === word.slice(wordSelection.from + 2, wordSelection.to - 2)
)?.value ?? "not found"
const result = parseTemplateStringE(envValue, aggregateEnvs)
const finalEnv = E.isLeft(result) ? "error" : result.right
return {
pos: start,
end: to,
above: true,
arrow: true,
create() {
const dom = document.createElement("span")
const xmp = document.createElement("xmp")
xmp.textContent = finalEnv
dom.appendChild(xmp)
dom.className = "tooltip-theme"
return { dom }
},
}
},
// HACK: This is a hack to fix hover tooltip not coming half of the time
// https://github.com/codemirror/tooltip/blob/765c463fc1d5afcc3ec93cee47d72606bed27e1d/src/tooltip.ts#L622
// Still doesn't fix the not showing up some of the time issue, but this is atleast more consistent
{ hoverTime: 1 } as any
)
function checkEnv(env: string, aggregateEnvs: HoppRESTVar[]) {
const className = aggregateEnvs.find(
(k: { key: string }) => k.key === env.slice(2, -2)
)
? HOPP_ENV_HIGHLIGHT_FOUND
: HOPP_ENV_HIGHLIGHT_NOT_FOUND
return Decoration.mark({
class: `${HOPP_ENV_HIGHLIGHT} ${className}`,
})
}
const getMatchDecorator = (aggregateEnvs: HoppRESTVar[]) =>
new MatchDecorator({
regexp: HOPP_ENVIRONMENT_REGEX,
decoration: (m) => checkEnv(m[0], aggregateEnvs),
})
export const environmentHighlightStyle = (aggregateEnvs: HoppRESTVar[]) => {
const decorator = getMatchDecorator(aggregateEnvs)
return ViewPlugin.define(
(view) => ({
decorations: decorator.createDeco(view),
update(u) {
this.decorations = decorator.updateDeco(u, this.decorations)
},
}),
{
decorations: (v) => v.decorations,
}
)
}
// export class HoppEnvironmentPlugin {
// private compartment = new Compartment()
//
// private envs: AggregateEnvironment[] = []
//
// constructor(
// subscribeToStream: StreamSubscriberFunc,
// private editorView: Ref<EditorView | undefined>
// ) {
// this.envs = getAggregateEnvs()
//
// subscribeToStream(aggregateEnvs$, (envs) => {
// this.envs = envs
//
// this.editorView.value?.dispatch({
// effects: this.compartment.reconfigure([
// cursorTooltipField(this.envs),
// environmentHighlightStyle(this.envs),
// ]),
// })
// })
// }
//
// get extension() {
// return this.compartment.of([
// cursorTooltipField(this.envs),
// environmentHighlightStyle(this.envs),
// ])
// }
// }
export class HoppReactiveVarPlugin {
private compartment = new Compartment()
private envs: HoppRESTVar[] = []
constructor(
envsRef: Ref<HoppRESTVar[]>,
private editorView: Ref<EditorView | undefined>
) {
watch(
envsRef,
(envs) => {
this.envs = envs
this.editorView.value?.dispatch({
effects: this.compartment.reconfigure([
cursorTooltipField(this.envs),
environmentHighlightStyle(this.envs),
]),
})
},
{ immediate: true }
)
}
get extension() {
return this.compartment.of([
cursorTooltipField(this.envs),
environmentHighlightStyle(this.envs),
])
}
}

View File

@@ -14,6 +14,7 @@ import {
HoppRESTHeader,
HoppRESTParam,
} from "@hoppscotch/data"
import { parseTemplateStringV } from "@hoppscotch/data/src/pathVariables"
import { arrayFlatMap, arraySort } from "../functional/array"
import { toFormData } from "../functional/formData"
import { tupleToRecord } from "../functional/record"
@@ -29,6 +30,7 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
effectiveFinalHeaders: { key: string; value: string }[]
effectiveFinalParams: { key: string; value: string }[]
effectiveFinalBody: FormData | string | null
effectiveFinalVars: { key: string; value: string }[]
}
/**
@@ -298,15 +300,21 @@ export function getEffectiveRESTRequest(
value: parseTemplateString(x.value, envVariables),
}))
)
const effectiveFinalVars = request.vars
const effectiveFinalBody = getFinalBodyFromRequest(request, envVariables)
return {
...request,
effectiveFinalURL: parseTemplateString(request.endpoint, envVariables),
effectiveFinalURL: parseTemplateStringV(
request.endpoint,
envVariables,
request.vars
),
effectiveFinalHeaders,
effectiveFinalParams,
effectiveFinalBody,
effectiveFinalVars,
}
}

View File

@@ -30,12 +30,7 @@ export const getDefaultRESTRequest = (): HoppRESTRequest => ({
endpoint: "https://echo.hoppscotch.io",
name: "Untitled request",
params: [],
vars: [
{
key: "amount",
value: "23",
},
],
vars: [],
headers: [],
method: "GET",
auth: {

View File

@@ -9,7 +9,14 @@ export type Environment = {
}[]
}
export type Variables = {
key: string
value: string
}[]
const REGEX_ENV_VAR = /<<([^>]*)>>/g // "<<myVariable>>"
const REGEX_PATHVAR = /{{([^>]*)}}/g // "{{myVariable}}"
/**
* How much times can we expand environment variables
@@ -59,9 +66,9 @@ export const parseBodyEnvVariables = (
export function parseTemplateStringE(
str: string,
variables: Environment["variables"]
variables: Environment["variables"],
) {
if (!variables || !str) {
if (!variables || !str ) {
return E.right(str)
}

View File

@@ -0,0 +1,76 @@
import { pipe } from "fp-ts/function"
import * as E from "fp-ts/Either"
import {parseTemplateStringE} from "./environment";
export type Environment = {
name: string
variables: {
key: string
value: string
}[]
}
export type Variables = {
key: string
value: string
}[]
const REGEX_ENV_VAR = /<<([^>]*)>>/g // "<<myVariable>>"
const REGEX_PATHVAR = /{{([^>]*)}}/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 parseTemplateStringEV(
str: string,
variables: Environment["variables"],
pathVariables: Variables
) {
if (!variables || !str || !pathVariables) {
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++
}
while (result.match(REGEX_PATHVAR) != null && depth <= ENV_MAX_EXPAND_LIMIT) {
result = decodeURI(encodeURI(result)).replace(
REGEX_PATHVAR,
(_, p1) => pathVariables.find((x) => x.key === p1)?.value || ""
)
}
return depth > ENV_MAX_EXPAND_LIMIT
? E.left(ENV_EXPAND_LOOP)
: E.right(result)
}
/**
* @deprecated Use `parseTemplateStringE` instead
*/
export const parseTemplateStringV = (
str: string,
variables: Environment["variables"],
pathVariables: Variables
) =>
pipe(
parseTemplateStringEV(str, variables, pathVariables),
E.getOrElse(() => str)
)