From e4d9f82a75f9f6ed1d107f7eadee85a90d6e4ddd Mon Sep 17 00:00:00 2001 From: Kishan Jadav Date: Mon, 30 Sep 2024 09:47:34 +0100 Subject: [PATCH] feat: support for predefined variables (#3886) Co-authored-by: Anwarul Islam Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com> --- .../hoppscotch-common/assets/scss/styles.scss | 14 + .../components/environments/my/Details.vue | 4 +- .../src/components/http/Headers.vue | 1 + .../src/components/http/Parameters.vue | 1 + .../src/components/http/RawBody.vue | 1 + .../src/components/http/RequestVariables.vue | 1 + .../src/components/http/URLEncodedParams.vue | 1 + .../src/components/smart/EnvInput.vue | 12 +- .../src/composables/codemirror.ts | 12 + .../extensions/HoppPredefinedVariables.ts | 142 +++++++ .../src/newstore/environments.ts | 38 +- .../hoppscotch-data/src/environment/index.ts | 27 +- packages/hoppscotch-data/src/index.ts | 1 + .../src/predefinedVariables.ts | 370 ++++++++++++++++++ 14 files changed, 608 insertions(+), 17 deletions(-) create mode 100644 packages/hoppscotch-common/src/helpers/editor/extensions/HoppPredefinedVariables.ts create mode 100644 packages/hoppscotch-data/src/predefinedVariables.ts diff --git a/packages/hoppscotch-common/assets/scss/styles.scss b/packages/hoppscotch-common/assets/scss/styles.scss index 258992de7..526992436 100644 --- a/packages/hoppscotch-common/assets/scss/styles.scss +++ b/packages/hoppscotch-common/assets/scss/styles.scss @@ -609,3 +609,17 @@ details[open] summary .indicator { .gql-operation-highlight { @apply opacity-100; } + +.predefined-variable-highlight { + color: inherit; + + &.predefined-variable-valid { + @apply bg-yellow-500; + @apply hover:bg-yellow-600; + } + + &.predefined-variable-invalid { + @apply hover:bg-red-300; + @apply bg-red-300; + } +} diff --git a/packages/hoppscotch-common/src/components/environments/my/Details.vue b/packages/hoppscotch-common/src/components/environments/my/Details.vue index 09ec87098..36d37cc59 100644 --- a/packages/hoppscotch-common/src/components/environments/my/Details.vue +++ b/packages/hoppscotch-common/src/components/environments/my/Details.vue @@ -261,7 +261,7 @@ const clearIcon = refAutoReset( 1000 ) -const globalVars = useReadonlyStream(globalEnv$, {} as GlobalEnvironment) +const globalEnv = useReadonlyStream(globalEnv$, {} as GlobalEnvironment) type SelectedEnv = "variables" | "secret" @@ -319,7 +319,7 @@ const liveEnvs = computed(() => { } return [ ...vars.value.map((x) => ({ ...x.env, source: editingName.value! })), - ...globalVars.value.variables.map((x) => ({ ...x, source: "Global" })), + ...globalEnv.value.variables.map((x) => ({ ...x, source: "Global" })), ] }) diff --git a/packages/hoppscotch-common/src/components/http/Headers.vue b/packages/hoppscotch-common/src/components/http/Headers.vue index 03936ae49..d7e2b110e 100644 --- a/packages/hoppscotch-common/src/components/http/Headers.vue +++ b/packages/hoppscotch-common/src/components/http/Headers.vue @@ -343,6 +343,7 @@ useCodemirror( linter, completer: null, environmentHighlights: true, + predefinedVariablesHighlights: true, }) ) diff --git a/packages/hoppscotch-common/src/components/http/Parameters.vue b/packages/hoppscotch-common/src/components/http/Parameters.vue index 8434150d7..3a0a12210 100644 --- a/packages/hoppscotch-common/src/components/http/Parameters.vue +++ b/packages/hoppscotch-common/src/components/http/Parameters.vue @@ -161,6 +161,7 @@ useCodemirror( linter, completer: null, environmentHighlights: true, + predefinedVariablesHighlights: true, }) ) diff --git a/packages/hoppscotch-common/src/components/http/RawBody.vue b/packages/hoppscotch-common/src/components/http/RawBody.vue index 7def623dc..42fc11c9f 100644 --- a/packages/hoppscotch-common/src/components/http/RawBody.vue +++ b/packages/hoppscotch-common/src/components/http/RawBody.vue @@ -160,6 +160,7 @@ useCodemirror( linter: langLinter, completer: null, environmentHighlights: true, + predefinedVariablesHighlights: true, }) ) diff --git a/packages/hoppscotch-common/src/components/http/RequestVariables.vue b/packages/hoppscotch-common/src/components/http/RequestVariables.vue index 352e390b5..3343a8634 100644 --- a/packages/hoppscotch-common/src/components/http/RequestVariables.vue +++ b/packages/hoppscotch-common/src/components/http/RequestVariables.vue @@ -234,6 +234,7 @@ useCodemirror( linter, completer: null, environmentHighlights: true, + predefinedVariablesHighlights: true, }) ) diff --git a/packages/hoppscotch-common/src/components/http/URLEncodedParams.vue b/packages/hoppscotch-common/src/components/http/URLEncodedParams.vue index 1048b6b54..2903b0c0c 100644 --- a/packages/hoppscotch-common/src/components/http/URLEncodedParams.vue +++ b/packages/hoppscotch-common/src/components/http/URLEncodedParams.vue @@ -246,6 +246,7 @@ useCodemirror( linter, completer: null, environmentHighlights: true, + predefinedVariablesHighlights: true, }) ) diff --git a/packages/hoppscotch-common/src/components/smart/EnvInput.vue b/packages/hoppscotch-common/src/components/smart/EnvInput.vue index 88a4469fc..f4a4297ba 100644 --- a/packages/hoppscotch-common/src/components/smart/EnvInput.vue +++ b/packages/hoppscotch-common/src/components/smart/EnvInput.vue @@ -78,6 +78,7 @@ import { clone } from "lodash-es" import { history, historyKeymap } from "@codemirror/commands" import { inputTheme } from "~/helpers/editor/themes/baseTheme" import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment" +import { HoppPredefinedVariablesPlugin } from "~/helpers/editor/extensions/HoppPredefinedVariables" import { useReadonlyStream } from "@composables/stream" import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments" import { platform } from "~/platform" @@ -103,6 +104,7 @@ const props = withDefaults( focus?: boolean selectTextOnMount?: boolean environmentHighlights?: boolean + predefinedVariablesHighlights?: boolean readonly?: boolean autoCompleteSource?: string[] inspectionResults?: InspectorResult[] | undefined @@ -118,6 +120,7 @@ const props = withDefaults( focus: false, readonly: false, environmentHighlights: true, + predefinedVariablesHighlights: true, autoCompleteSource: undefined, inspectionResult: undefined, inspectionResults: undefined, @@ -396,20 +399,22 @@ function envAutoCompletion(context: CompletionContext) { info: env?.value ?? "", apply: env?.key ? `<<${env.key}>>` : "", })) - .filter((x) => x) + .filter(Boolean) const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1) const textBefore = context.state.sliceDoc(nodeBefore.from, context.pos) - const tagBefore = /<<\w*$/.exec(textBefore) + const tagBefore = /<<\$?\w*$/.exec(textBefore) // Update regex to match <<$ as well + if (!tagBefore && !context.explicit) return null return { from: tagBefore ? nodeBefore.from + tagBefore.index : context.pos, options: options, - validFor: /^(<<\w*)?$/, + validFor: /^(<<\$?\w*)?$/, } } const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view) +const predefinedVariablePlugin = new HoppPredefinedVariablesPlugin() function handleTextSelection() { const selection = view.value?.state.selection.main @@ -490,6 +495,7 @@ const getExtensions = (readonly: boolean): Extension => { position: "absolute", }), props.environmentHighlights ? envTooltipPlugin : [], + props.predefinedVariablesHighlights ? predefinedVariablePlugin : [], placeholderExt(props.placeholder), EditorView.domEventHandlers({ paste(ev) { diff --git a/packages/hoppscotch-common/src/composables/codemirror.ts b/packages/hoppscotch-common/src/composables/codemirror.ts index 713a82852..bef6de5a8 100644 --- a/packages/hoppscotch-common/src/composables/codemirror.ts +++ b/packages/hoppscotch-common/src/composables/codemirror.ts @@ -47,6 +47,7 @@ import { useDebounceFn } from "@vueuse/core" // TODO: Migrate from legacy mode import * as E from "fp-ts/Either" +import { HoppPredefinedVariablesPlugin } from "~/helpers/editor/extensions/HoppPredefinedVariables" type ExtendedEditorConfig = { mode: string @@ -63,6 +64,12 @@ type CodeMirrorOptions = { // NOTE: This property is not reactive environmentHighlights: boolean + /** + * Whether or not to highlight predefined variables, such as: `<<$guid>>`. + * - These are special variables that starts with a dolar sign. + */ + predefinedVariablesHighlights?: boolean + additionalExts?: Extension[] contextMenuEnabled?: boolean @@ -251,6 +258,10 @@ export function useCodemirror( text: null, }) } + const predefinedVariable: HoppPredefinedVariablesPlugin | null = + options.predefinedVariablesHighlights + ? new HoppPredefinedVariablesPlugin() + : null function handleTextSelection() { const selection = view.value?.state.selection.main @@ -396,6 +407,7 @@ export function useCodemirror( ] if (environmentTooltip) extensions.push(environmentTooltip.extension) + if (predefinedVariable) extensions.push(predefinedVariable.extension) view.value = new EditorView({ parent: el, diff --git a/packages/hoppscotch-common/src/helpers/editor/extensions/HoppPredefinedVariables.ts b/packages/hoppscotch-common/src/helpers/editor/extensions/HoppPredefinedVariables.ts new file mode 100644 index 000000000..1405d000a --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/editor/extensions/HoppPredefinedVariables.ts @@ -0,0 +1,142 @@ +import { Compartment } from "@codemirror/state" +import { + Decoration, + MatchDecorator, + ViewPlugin, + hoverTooltip, +} from "@codemirror/view" +import IconSquareAsterisk from "~icons/lucide/square-asterisk?raw" +import { HOPP_SUPPORTED_PREDEFINED_VARIABLES } from "@hoppscotch/data" + +const HOPP_PREDEFINED_VARIABLES_REGEX = /(<<\$[a-zA-Z0-9-_]+>>)/g + +const HOPP_PREDEFINED_VARIABLE_HIGHLIGHT = + "cursor-help transition rounded px-1 focus:outline-none mx-0.5 predefined-variable-highlight" +const HOPP_PREDEFINED_VARIABLE_HIGHLIGHT_VALID = "predefined-variable-valid" +const HOPP_PREDEFINED_VARIABLE_HIGHLIGHT_INVALID = "predefined-variable-invalid" + +const getMatchDecorator = () => { + return new MatchDecorator({ + regexp: HOPP_PREDEFINED_VARIABLES_REGEX, + decoration: (m) => checkPredefinedVariable(m[0]), + }) +} + +const cursorTooltipField = () => + 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 - 3, + // wordSelection.to + 2 + // ) + // if (!HOPP_PREDEFINED_VARIABLES_REGEX.test(word)) return null + + // Tracking the start and the end of the words + let start = pos + let end = pos + + while (start > from && /[a-zA-Z0-9-_]+/.test(text[start - from - 1])) + start-- + while (end < to && /[a-zA-Z0-9-_]+/.test(text[end - from])) end++ + + if ( + (start === pos && side < 0) || + (end === pos && side > 0) || + !HOPP_PREDEFINED_VARIABLES_REGEX.test( + text.slice(start - from - 3, end - from + 2) + ) + ) { + return null + } + + const variableName = text.slice(start - from - 1, end - from) + + const variable = HOPP_SUPPORTED_PREDEFINED_VARIABLES.find( + (VARIABLE) => VARIABLE.key === variableName + ) + + const variableIcon = `${IconSquareAsterisk}` + const variableDescription = + variable !== undefined + ? `${variableName} - ${variable.description}` + : `${variableName} is not a valid predefined variable.` + + return { + pos: start, + end: to, + above: true, + arrow: true, + create() { + const dom = document.createElement("div") + dom.className = "tippy-box" + dom.dataset.theme = "tooltip" + + const icon = document.createElement("span") + icon.innerHTML = variableIcon + icon.className = "mr-2" + + const tooltipContainer = document.createElement("span") + tooltipContainer.className = "tippy-content" + + tooltipContainer.appendChild(icon) + tooltipContainer.appendChild( + document.createTextNode(variableDescription) + ) + + dom.appendChild(tooltipContainer) + 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 + ) + +const checkPredefinedVariable = (variable: string) => { + const inputVariableKey = variable.slice(2, -2) + + const className = HOPP_SUPPORTED_PREDEFINED_VARIABLES.find((v) => { + return v.key === inputVariableKey + }) + ? HOPP_PREDEFINED_VARIABLE_HIGHLIGHT_VALID + : HOPP_PREDEFINED_VARIABLE_HIGHLIGHT_INVALID + + return Decoration.mark({ + class: `${HOPP_PREDEFINED_VARIABLE_HIGHLIGHT} ${className}`, + }) +} + +export const predefinedVariableHighlightStyle = () => { + const decorator = getMatchDecorator() + + return ViewPlugin.define( + (view) => ({ + decorations: decorator.createDeco(view), + update(u) { + this.decorations = decorator.updateDeco(u, this.decorations) + }, + }), + { + decorations: (v) => v.decorations, + } + ) +} + +export class HoppPredefinedVariablesPlugin { + private compartment = new Compartment() + + get extension() { + return this.compartment.of([ + cursorTooltipField(), + predefinedVariableHighlightStyle(), + ]) + } +} diff --git a/packages/hoppscotch-common/src/newstore/environments.ts b/packages/hoppscotch-common/src/newstore/environments.ts index 3c04da409..6e3c3bf70 100644 --- a/packages/hoppscotch-common/src/newstore/environments.ts +++ b/packages/hoppscotch-common/src/newstore/environments.ts @@ -2,6 +2,7 @@ import { Environment, GlobalEnvironment, GlobalEnvironmentVariable, + HOPP_SUPPORTED_PREDEFINED_VARIABLES, } from "@hoppscotch/data" import { cloneDeep, isEqual } from "lodash-es" import { combineLatest, Observable } from "rxjs" @@ -407,24 +408,45 @@ export type AggregateEnvironment = { export const aggregateEnvs$: Observable = combineLatest( [currentEnvironment$, globalEnv$] ).pipe( - map(([selectedEnv, globalVars]) => { - const results: AggregateEnvironment[] = [] + map(([selectedEnv, globalEnv]) => { + const effectiveAggregateEnvs: AggregateEnvironment[] = [] + + // Ensure pre-defined variables are prioritised over other environment variables with the same name + HOPP_SUPPORTED_PREDEFINED_VARIABLES.forEach(({ key, getValue }) => { + effectiveAggregateEnvs.push({ + key, + value: getValue(), + secret: false, + sourceEnv: selectedEnv?.name ?? "Global", + }) + }) + + const aggregateEnvKeys = effectiveAggregateEnvs.map(({ key }) => key) selectedEnv?.variables.forEach((variable) => { const { key, secret } = variable const value = "value" in variable ? variable.value : "" - results.push({ key, value, secret, sourceEnv: selectedEnv.name }) + if (!aggregateEnvKeys.includes(key)) { + effectiveAggregateEnvs.push({ + key, + value, + secret, + sourceEnv: selectedEnv.name, + }) + } }) - globalVars.variables.forEach((variable) => { + globalEnv.variables.forEach((variable) => { const { key, secret } = variable const value = "value" in variable ? variable.value : "" - results.push({ key, value, secret, sourceEnv: "Global" }) + if (!aggregateEnvKeys.includes(key)) { + effectiveAggregateEnvs.push({ key, value, secret, sourceEnv: "Global" }) + } }) - return results + return effectiveAggregateEnvs }), distinctUntilChanged(isEqual) ) @@ -503,7 +525,7 @@ export function getAggregateEnvsWithSecrets() { export const aggregateEnvsWithSecrets$: Observable = combineLatest([currentEnvironment$, globalEnv$]).pipe( - map(([selectedEnv, globalVars]) => { + map(([selectedEnv, globalEnv]) => { const results: AggregateEnvironment[] = [] selectedEnv?.variables.map((x, index) => { let value @@ -523,7 +545,7 @@ export const aggregateEnvsWithSecrets$: Observable = }) }) - globalVars.variables.map((x, index) => { + globalEnv.variables.map((x, index) => { let value if (x.secret) { value = secretEnvironmentService.getSecretEnvironmentVariableValue( diff --git a/packages/hoppscotch-data/src/environment/index.ts b/packages/hoppscotch-data/src/environment/index.ts index 46c43aa82..dbd4954c1 100644 --- a/packages/hoppscotch-data/src/environment/index.ts +++ b/packages/hoppscotch-data/src/environment/index.ts @@ -6,6 +6,7 @@ import { z } from "zod" import V0_VERSION from "./v/0" import V1_VERSION, { uniqueID } from "./v/1" +import { HOPP_SUPPORTED_PREDEFINED_VARIABLES } from "../predefinedVariables" const versionedObject = z.object({ v: z.number(), @@ -58,12 +59,21 @@ export function parseBodyEnvVariablesE( while (result.match(REGEX_ENV_VAR) != null && depth <= ENV_MAX_EXPAND_LIMIT) { result = result.replace(REGEX_ENV_VAR, (key) => { - const found = env.find( - (envVar) => envVar.key === key.replace(/[<>]/g, "") + const variableName = key.replace(/[<>]/g, "") + + // Prioritise predefined variable values over normal environment variables processing. + const foundPredefinedVar = HOPP_SUPPORTED_PREDEFINED_VARIABLES.find( + (preVar) => preVar.key === variableName ) - if (found && "value" in found) { - return found.value + if (foundPredefinedVar) { + return foundPredefinedVar.getValue() + } + + const foundEnv = env.find((envVar) => envVar.key === variableName) + + if (foundEnv && "value" in foundEnv) { + return foundEnv.value } return key }) @@ -110,6 +120,15 @@ export function parseTemplateStringE( !isSecret ) { result = decodeURI(encodeURI(result)).replace(REGEX_ENV_VAR, (_, p1) => { + // Prioritise predefined variable values over normal environment variables processing. + const foundPredefinedVar = HOPP_SUPPORTED_PREDEFINED_VARIABLES.find( + (preVar) => preVar.key === p1 + ) + + if (foundPredefinedVar) { + return foundPredefinedVar.getValue() + } + const variable = variables.find((x) => x && x.key === p1) if (variable && "value" in variable) { diff --git a/packages/hoppscotch-data/src/index.ts b/packages/hoppscotch-data/src/index.ts index c37534c05..4576e4560 100644 --- a/packages/hoppscotch-data/src/index.ts +++ b/packages/hoppscotch-data/src/index.ts @@ -4,3 +4,4 @@ export * from "./collection" export * from "./rawKeyValue" export * from "./environment" export * from "./global-environment" +export * from "./predefinedVariables" diff --git a/packages/hoppscotch-data/src/predefinedVariables.ts b/packages/hoppscotch-data/src/predefinedVariables.ts new file mode 100644 index 000000000..07d219496 --- /dev/null +++ b/packages/hoppscotch-data/src/predefinedVariables.ts @@ -0,0 +1,370 @@ +export type PredefinedVariable = { + key: `$${string}` + description: string + getValue: () => string +} + +export const HOPP_SUPPORTED_PREDEFINED_VARIABLES: PredefinedVariable[] = [ + // Common + { + key: "$guid", + description: "A v4 style GUID.", + getValue: () => { + const characters = "0123456789abcdef" + let guid = "" + for (let i = 0; i < 36; i++) { + if (i === 8 || i === 13 || i === 18 || i === 23) { + guid += "-" + } else if (i === 14) { + guid += "4" + } else if (i === 19) { + guid += characters.charAt(8 + Math.floor(Math.random() * 4)) + } else { + guid += characters.charAt( + Math.floor(Math.random() * characters.length) + ) + } + } + return guid + }, + }, + { + key: "$nowISO", + description: "Current date and time in ISO-8601 format.", + getValue: () => new Date().toISOString(), + }, + { + key: "$timestamp", + description: "The current UNIX timestamp in seconds.", + getValue: () => Math.floor(Date.now() / 1000).toString(), + }, + + { + key: "$isoTimestamp", + description: "The current ISO timestamp at zero UTC.", + getValue: () => new Date().toISOString(), + }, + { + key: "$randomUUID", + description: "A random 36-character UUID.", + getValue: () => { + const characters = "0123456789abcdef" + let uuid = "" + for (let i = 0; i < 36; i++) { + if (i === 8 || i === 13 || i === 18 || i === 23) { + uuid += "-" + } else { + uuid += characters.charAt( + Math.floor(Math.random() * characters.length) + ) + } + } + return uuid + }, + }, + + // Text, numbers, and colors + { + key: "$randomAlphaNumeric", + description: "A random alpha-numeric character.", + getValue: () => { + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + return characters.charAt(Math.floor(Math.random() * characters.length)) + }, + }, + + { + key: "$randomBoolean", + description: "A random boolean value.", + getValue: () => (Math.random() < 0.5 ? "true" : "false"), + }, + + { + key: "$randomInt", + description: "A random integer between 0 and 1000.", + getValue: () => Math.floor(Math.random() * 1000).toString(), + }, + + { + key: "$randomColor", + description: "A random color.", + getValue: () => { + const colors = ["red", "green", "blue", "yellow", "purple", "orange"] + return colors[Math.floor(Math.random() * colors.length)] + }, + }, + + { + key: "$randomHexColor", + description: "A random hex value.", + getValue: () => { + const characters = "0123456789abcdef" + let color = "#" + for (let i = 0; i < 6; i++) { + color += characters.charAt( + Math.floor(Math.random() * characters.length) + ) + } + return color + }, + }, + + { + key: "$randomAbbreviation", + description: "A random abbreviation.", + getValue: () => { + const abbreviations = [ + "SQL", + "PCI", + "JSON", + "HTML", + "CSS", + "JS", + "TS", + "API", + ] + return abbreviations[Math.floor(Math.random() * abbreviations.length)] + }, + }, + + // Internet and IP addresses + { + key: "$randomIP", + description: "A random IPv4 address.", + getValue: () => { + const ip = Array.from({ length: 4 }, () => + Math.floor(Math.random() * 256) + ) + return ip.join(".") + }, + }, + + { + key: "$randomIPV6", + description: "A random IPv6 address.", + getValue: () => { + const ip = Array.from({ length: 8 }, () => + Math.floor(Math.random() * 65536).toString(16) + ) + return ip.join(":") + }, + }, + + { + key: "$randomMACAddress", + description: "A random MAC address.", + getValue: () => { + const mac = Array.from({ length: 6 }, () => + Math.floor(Math.random() * 256).toString(16) + ) + return mac.join(":") + }, + }, + + { + key: "$randomPassword", + description: "A random 15-character alpha-numeric password.", + getValue: () => { + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + let password = "" + for (let i = 0; i < 15; i++) { + password += characters.charAt( + Math.floor(Math.random() * characters.length) + ) + } + return password + }, + }, + + { + key: "$randomLocale", + description: "A random two-letter language code (ISO 639-1).", + getValue: () => { + const locales = ["ny", "sr", "si"] + return locales[Math.floor(Math.random() * locales.length)] + }, + }, + + { + key: "$randomUserAgent", + description: "A random user agent.", + getValue: () => { + const userAgents = [ + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.9.8; rv:15.6) Gecko/20100101 Firefox/15.6.6", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:15.6) Gecko/20100101 Firefox/15.6.6", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:15.6) Gecko/20100101 Firefox/15.6.6", + ] + return userAgents[Math.floor(Math.random() * userAgents.length)] + }, + }, + { + key: "$randomProtocol", + description: "A random internet protocol.", + getValue: () => { + const protocols = ["http", "https"] + return protocols[Math.floor(Math.random() * protocols.length)] + }, + }, + + { + key: "$randomSemver", + description: "A random semantic version number.", + getValue: () => { + const semver = Array.from({ length: 3 }, () => + Math.floor(Math.random() * 10) + ) + return semver.join(".") + }, + }, + + // Names + { + key: "$randomFirstName", + description: "A random first name.", + getValue: () => { + const firstNames = [ + "Ethan", + "Chandler", + "Megane", + "John", + "Jane", + "Alice", + "Bob", + ] + return firstNames[Math.floor(Math.random() * firstNames.length)] + }, + }, + { + key: "$randomLastName", + description: "A random last name.", + getValue: () => { + const lastNames = [ + "Schaden", + "Schneider", + "Willms", + "Doe", + "Smith", + "Johnson", + ] + return lastNames[Math.floor(Math.random() * lastNames.length)] + }, + }, + { + key: "$randomFullName", + description: "A random first and last name.", + getValue: () => { + const firstNames = [ + "Ethan", + "Chandler", + "Megane", + "John", + "Jane", + "Alice", + "Bob", + ] + const lastNames = [ + "Schaden", + "Schneider", + "Willms", + "Doe", + "Smith", + "Johnson", + ] + return `${firstNames[Math.floor(Math.random() * firstNames.length)]} ${ + lastNames[Math.floor(Math.random() * lastNames.length)] + }` + }, + }, + { + key: "$randomNamePrefix", + description: "A random name prefix.", + getValue: () => { + const prefixes = ["Dr.", "Ms.", "Mr.", "Mrs.", "Miss", "Prof."] + return prefixes[Math.floor(Math.random() * prefixes.length)] + }, + }, + { + key: "$randomNameSuffix", + description: "A random name suffix.", + getValue: () => { + const suffixes = ["I", "MD", "DDS", "PhD", "Esq.", "Jr."] + return suffixes[Math.floor(Math.random() * suffixes.length)] + }, + }, + + // Addresses + { + key: "$randomCity", + description: "A random city name.", + getValue: () => { + const cities = [ + "New York", + "Los Angeles", + "Chicago", + "Houston", + "Phoenix", + "Philadelphia", + ] + return cities[Math.floor(Math.random() * cities.length)] + }, + }, + + // profession + { + key: "$randomJobArea", + description: "A random job area.", + getValue: () => { + const jobAreas = [ + "Mobility", + "Intranet", + "Configuration", + "Development", + "Design", + "Testing", + ] + return jobAreas[Math.floor(Math.random() * jobAreas.length)] + }, + }, + { + key: "$randomJobDescriptor", + description: "A random job descriptor.", + getValue: () => { + const jobDescriptors = [ + "Forward", + "Corporate", + "Senior", + "Junior", + "Lead", + "Principal", + ] + return jobDescriptors[Math.floor(Math.random() * jobDescriptors.length)] + }, + }, + { + key: "$randomJobTitle", + description: "A random job title.", + getValue: () => { + const jobTitles = [ + "International Creative Liaison", + "Global Branding Officer", + "Dynamic Data Specialist", + "Internal Communications Consultant", + "Productivity Analyst", + "Regional Applications Developer", + ] + return jobTitles[Math.floor(Math.random() * jobTitles.length)] + }, + }, + { + key: "$randomJobType", + description: "A random job type.", + getValue: () => { + const jobTypes = ["Supervisor", "Manager", "Coordinator", "Director"] + return jobTypes[Math.floor(Math.random() * jobTypes.length)] + }, + }, + + // TODO: Support various other predefined variables +]