Skip to content

Commit b55aa79

Browse files
authored
Add Prompt Variables (#616)
1 parent c2006ee commit b55aa79

File tree

9 files changed

+227
-22
lines changed

9 files changed

+227
-22
lines changed

README.md

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ REST Client allows you to send HTTP request and view the response in Visual Stud
2424
- AWS Signature v4
2525
* Environments and custom/system variables support
2626
- Use variables in any place of request(_URL_, _Headers_, _Body_)
27-
- Support both __environment__, __file__ and __request__ custom variables
27+
- Support __environment__, __file__, __request__ and __prompt__ custom variables
28+
- Interactively assign __prompt__ custom variables per request
2829
- Auto completion and hover support for both __environment__, __file__ and __request__ custom variables
29-
- Diagnostic support for __request__ and __file__ custom variables
30+
- Diagnostic support for __request__, __file__ and __prompt__ custom variables
3031
- Go to definition support for __request__ and __file__ custom variables
3132
- Find all references support _ONLY_ for __file__ custom variables
3233
- Provide system dynamic variables
@@ -413,7 +414,7 @@ Environments give you the ability to customize requests using variables, and you
413414
Environments and including variables are defined directly in `Visual Studio Code` setting file, so you can create/update/delete environments and variables at any time you wish. If you __DO NOT__ want to use any environment, you can choose `No Environment` in the environment list. Notice that if you select `No Environment`, variables defined in shared environment are still available. See [Environment Variables](#environment-variables) for more details about environment variables.
414415

415416
## Variables
416-
We support two types of variables, one is __Custom Variables__ which is defined by user and can be further divided into __Environment Variables__, __File Variables__ and __Request Variables__, the other is __System Variables__ which is a predefined set of variables out-of-box.
417+
We support two types of variables, one is __Custom Variables__ which is defined by user and can be further divided into __Environment Variables__, __File Variables__, __Prompt Variables__, and __Request Variables__, the other is __System Variables__ which is a predefined set of variables out-of-box.
417418

418419
The reference syntax of system and custom variables types has a subtle difference, for the former the syntax is `{{$SystemVariableName}}`, while for the latter the syntax is `{{CustomVariableName}}`, without preceding `$` before variable name. The definition syntax and location for different types of custom variables are different. Notice that when the same name used for custom variables, request variables takes higher resolving precedence over file variables, file variables takes higher precedence over environment variables.
419420

@@ -480,6 +481,32 @@ Content-Type: {{contentType}}
480481
481482
```
482483

484+
#### Prompt Variables
485+
With prompt variables, user can input the variables to be used when sending a request. This gives a flexibility to change most dynamic variables without having to change the `http` file. User can specify more than one prompt variables. The definition syntax of prompt variables is like a single-line comment by adding the syntax before the desired request url with the following syntax __`// @prompt {var1}`__ or __`# @prompt {var1}`__. A variable description is also assignable using __`// @prompt {var1} {description}`__ or __`# @prompt {var1} {description}`__ which will prompt an input popup with a desired description message.
486+
487+
488+
The reference syntax is the same as others, follows __`{{var}}`__. The prompt variable will override any preceding assigned variable and will never be stored to be used in other requests.
489+
490+
```http
491+
@hostname = api.example.com
492+
@port = 8080
493+
@host = {{hostname}}:{{port}}
494+
@contentType = application/json
495+
496+
###
497+
# @prompt username
498+
# @prompt refCode Your reference code display on webpage
499+
# @prompt otp Your one-time password in your mailbox
500+
POST https://{{host}}/verify-otp/{{refCode}} HTTP/1.1
501+
Content-Type: {{contentType}}
502+
503+
{
504+
"username": "{{username}}",
505+
"otp": "{{otp}}"
506+
}
507+
508+
```
509+
483510
#### Request Variables
484511
Request variables are similar to file variables in some aspects like scope and definition location. However, they have some obvious differences. The definition syntax of request variables is just like a single-line comment, and follows __`// @name requestName`__ or __`# @name requestName`__ just before the desired request url. You can think of request variable as attaching a *name metadata* to the underlying request, and this kind of requests can be called with **Named Request**, while normal requests can be called with **Anonymous Request**. Other requests can use `requestName` as an identifier to reference the expected part of the named request or its latest response. Notice that if you want to refer the response of a named request, you need to manually trigger the named request to retrieve its response first, otherwise the plain text of variable reference like `{{requestName.response.body.$.id}}` will be sent instead.
485512

src/common/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,6 @@ export const RequestVariableDefinitionWithNameRegexFactory = (name: string, flag
7070

7171
export const RequestVariableDefinitionRegex: RegExp = RequestVariableDefinitionWithNameRegexFactory("\\w+", "m");
7272

73+
export const PromptCommentRegex = /^\s*(?:#{1,}|\/{2,})\s*@prompt\s+([^\s]+)(?:\s+(.*))?\s*$/;
74+
7375
export const LineSplitterRegex: RegExp = /\r?\n/g;

src/models/httpElement.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,6 @@ export enum ElementType {
3131
SystemVariable,
3232
EnvironmentCustomVariable,
3333
FileCustomVariable,
34-
RequestCustomVariable
34+
RequestCustomVariable,
35+
PromptVariable
3536
}

src/models/requestMetadata.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ export enum RequestMetadata {
1616
* Represents the cookie jar is disabled for this request
1717
*/
1818
NoCookieJar = 'no-cookie-jar',
19+
20+
/**
21+
* Used to allow user to interactively input variables for this request
22+
*/
23+
Prompt = 'prompt',
1924
}
2025

2126
export function fromString(value: string): RequestMetadata | undefined {

src/providers/customVariableDiagnosticsProvider.ts

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { disposeAll } from '../utils/dispose';
88
import { RequestVariableCache } from "../utils/requestVariableCache";
99
import { RequestVariableCacheValueProcessor } from "../utils/requestVariableCacheValueProcessor";
1010
import { Selector } from '../utils/selector';
11+
1112
import { VariableProcessor } from "../utils/variableProcessor";
1213

1314
interface VariableWithPosition {
@@ -17,6 +18,11 @@ interface VariableWithPosition {
1718
end: Position;
1819
}
1920

21+
interface PromptVariableDefinitionWithRange {
22+
name: string;
23+
range: [number, number];
24+
}
25+
2026
export class CustomVariableDiagnosticsProvider {
2127
private httpDiagnosticCollection: DiagnosticCollection = languages.createDiagnosticCollection();
2228

@@ -82,16 +88,20 @@ export class CustomVariableDiagnosticsProvider {
8288
const diagnostics: Diagnostic[] = [];
8389

8490
const allAvailableVariables = await VariableProcessor.getAllVariablesDefinitions(document);
91+
const promptVariableDefinitions = this.findPromptVariableDefinitions(document);
8592
const variableReferences = this.findVariableReferences(document);
8693

8794
// Variable not found
8895
[...variableReferences.entries()]
8996
.filter(([name]) => !allAvailableVariables.has(name))
9097
.forEach(([, variables]) => {
91-
variables.forEach(({name, begin, end}) => {
92-
diagnostics.push(
93-
new Diagnostic(new Range(begin, end), `${name} is not found`, DiagnosticSeverity.Error));
94-
});
98+
variables
99+
.filter(variable => !this.hasPromptVariableDefintion(promptVariableDefinitions, variable))
100+
.forEach(({ name, begin, end }) => {
101+
diagnostics.push(
102+
new Diagnostic(new Range(begin, end), `${name} is not found`, DiagnosticSeverity.Error));
103+
104+
});
95105
});
96106

97107
// Request variable not active
@@ -101,7 +111,7 @@ export class CustomVariableDiagnosticsProvider {
101111
&& allAvailableVariables.get(name)![0] === VariableType.Request
102112
&& !RequestVariableCache.has(document, name))
103113
.forEach(([, variables]) => {
104-
variables.forEach(({name, begin, end}) => {
114+
variables.forEach(({ name, begin, end }) => {
105115
diagnostics.push(
106116
new Diagnostic(new Range(begin, end), `Request '${name}' has not been sent`, DiagnosticSeverity.Information));
107117
});
@@ -115,7 +125,7 @@ export class CustomVariableDiagnosticsProvider {
115125
&& RequestVariableCache.has(document, name))
116126
.forEach(([name, variables]) => {
117127
const value = RequestVariableCache.get(document, name);
118-
variables.forEach(({path, begin, end}) => {
128+
variables.forEach(({ path, begin, end }) => {
119129
path = path.replace(/^\{{2}\s*/, '').replace(/\s*\}{2}$/, '');
120130
const result = RequestVariableCacheValueProcessor.resolveRequestVariable(value, path);
121131
if (result.state !== ResolveState.Success) {
@@ -166,4 +176,33 @@ export class CustomVariableDiagnosticsProvider {
166176

167177
return vars;
168178
}
179+
180+
private findPromptVariableDefinitions(document: TextDocument): Map<string, PromptVariableDefinitionWithRange[]> {
181+
const defs = new Map<string, PromptVariableDefinitionWithRange[]>();
182+
const rawLines = document.getText().split(Constants.LineSplitterRegex);
183+
const requestRanges = Selector.getRequestRanges(rawLines, { ignoreCommentLine: false });
184+
for (const [start, end] of requestRanges) {
185+
const scopedLines = rawLines.slice(start, end + 1);
186+
for (const line of scopedLines) {
187+
const matched = line.match(Constants.PromptCommentRegex);
188+
if (matched) {
189+
const name = matched[1];
190+
if (defs.has(name)) {
191+
defs.get(name)!.push({ name, range: [start, end] });
192+
} else {
193+
defs.set(name, [{ name, range: [start, end] }]);
194+
}
195+
}
196+
}
197+
}
198+
return defs;
199+
}
200+
private hasPromptVariableDefintion(defs: Map<string, PromptVariableDefinitionWithRange[]>, variable: VariableWithPosition): boolean {
201+
const { name, begin, end } = variable;
202+
return defs.get(name)?.some(({ name, range: [rangeStart, rangeEnd] }) => {
203+
return name === name
204+
&& rangeStart <= begin.line
205+
&& end.line <= rangeEnd;
206+
}) || false;
207+
}
169208
}

src/utils/httpElementFactory.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import * as url from 'url';
2-
import { MarkdownString, SnippetString, TextDocument } from 'vscode';
2+
import { MarkdownString, SnippetString, TextDocument, window } from 'vscode';
33
import * as Constants from '../common/constants';
44
import { ElementType, HttpElement } from '../models/httpElement';
5+
import { RequestMetadata } from '../models/requestMetadata';
56
import { EnvironmentVariableProvider } from './httpVariableProviders/environmentVariableProvider';
67
import { FileVariableProvider } from './httpVariableProviders/fileVariableProvider';
78
import { RequestVariableProvider } from './httpVariableProviders/requestVariableProvider';
9+
import { Selector } from './selector';
810
import { UserDataManager } from './userDataManager';
911

1012
export class HttpElementFactory {
@@ -180,6 +182,32 @@ export class HttpElementFactory {
180182
new SnippetString(`{{${name}.\${1|request,response|}.\${2|headers,body|}.\${3:Header Name, *(Full Body), JSONPath or XPath}}}`)));
181183
}
182184

185+
// add active editor elements
186+
const editor = window.activeTextEditor;
187+
if (editor) {
188+
const activeLine = editor.selection.active.line;
189+
const selectedText = Selector.getDelimitedText(editor.document.getText(), activeLine);
190+
191+
if (selectedText) {
192+
// convert request text into lines
193+
const lines = selectedText.split(Constants.LineSplitterRegex);
194+
const metadatas = Selector.parseReqMetadatas(lines);
195+
196+
// add prompt variables
197+
const promptVariablesDefinitions = Selector.parsePromptMetadataForVariableDefinitions(metadatas.get(RequestMetadata.Prompt));
198+
for (const { name, description } of promptVariablesDefinitions) {
199+
const v = new MarkdownString(`${description ? `Description: ${description}` : `Prompt Variable: \`${name}\``}`);
200+
originalElements.push(
201+
new HttpElement(
202+
name,
203+
ElementType.PromptVariable,
204+
'^\\s*[^@]',
205+
v,
206+
new SnippetString(`{{${name}}}`)));
207+
}
208+
}
209+
}
210+
183211
// add urls from history
184212
const historyItems = await UserDataManager.getRequestHistory();
185213
const distinctRequestUrls = new Set(historyItems.map(item => item.url));
@@ -216,4 +244,5 @@ export class HttpElementFactory {
216244

217245
return elements;
218246
}
247+
219248
}

src/utils/selector.ts

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { EOL } from 'os';
2-
import { Range, TextEditor } from 'vscode';
2+
import { Range, TextEditor, window } from 'vscode';
33
import * as Constants from '../common/constants';
44
import { fromString as ParseReqMetaKey, RequestMetadata } from '../models/requestMetadata';
55
import { SelectedRequest } from '../models/SelectedRequest';
@@ -12,6 +12,11 @@ export interface RequestRangeOptions {
1212
ignoreResponseRange?: boolean;
1313
}
1414

15+
interface PromptVariableDefinition {
16+
name: string;
17+
description?: string;
18+
}
19+
1520
export class Selector {
1621
private static readonly responseStatusLineRegex = /^\s*HTTP\/[\d.]+/;
1722

@@ -38,6 +43,13 @@ export class Selector {
3843
// parse request metadata
3944
const metadatas = this.parseReqMetadatas(lines);
4045

46+
// process #@prompt comment metadata
47+
const promptVariablesDefinitions = this.parsePromptMetadataForVariableDefinitions(metadatas.get(RequestMetadata.Prompt));
48+
const promptVariables = await this.promptForInput(promptVariablesDefinitions);
49+
if (!promptVariables) {
50+
return null;
51+
}
52+
4153
// parse actual request lines
4254
const rawLines = lines.filter(l => !this.isCommentLine(l));
4355
const requestRange = this.getRequestRanges(rawLines)[0];
@@ -48,7 +60,7 @@ export class Selector {
4860
selectedText = rawLines.slice(requestRange[0], requestRange[1] + 1).join(EOL);
4961

5062
// variables replacement
51-
selectedText = await VariableProcessor.processRawRequest(selectedText);
63+
selectedText = await VariableProcessor.processRawRequest(selectedText, promptVariables);
5264

5365
return {
5466
text: selectedText,
@@ -78,19 +90,24 @@ export class Selector {
7890
const metaValue = matched[2];
7991
const metadata = ParseReqMetaKey(metaKey);
8092
if (metadata) {
81-
metadatas.set(metadata, metaValue || undefined);
93+
if (metadata === RequestMetadata.Prompt) {
94+
this.handlePromptMetadata(metadatas, line);
95+
} else {
96+
metadatas.set(metadata, metaValue || undefined);
97+
}
8298
}
8399
}
84100
return metadatas;
85101
}
86102

87103
public static getRequestRanges(lines: string[], options?: RequestRangeOptions): [number, number][] {
88104
options = {
89-
ignoreCommentLine: true,
90-
ignoreEmptyLine: true,
91-
ignoreFileVariableDefinitionLine: true,
92-
ignoreResponseRange: true,
93-
...options};
105+
ignoreCommentLine: true,
106+
ignoreEmptyLine: true,
107+
ignoreFileVariableDefinitionLine: true,
108+
ignoreResponseRange: true,
109+
...options
110+
};
94111
const requestRanges: [number, number][] = [];
95112
const delimitedLines = this.getDelimiterRows(lines);
96113
delimitedLines.push(lines.length);
@@ -153,7 +170,31 @@ export class Selector {
153170
return matched?.[1];
154171
}
155172

156-
private static getDelimitedText(fullText: string, currentLine: number): string | null {
173+
public static getPrompVariableDefinition(text: string): PromptVariableDefinition | undefined {
174+
const matched = text.match(Constants.PromptCommentRegex);
175+
if (matched) {
176+
const name = matched[1];
177+
const description = matched[2];
178+
return { name, description };
179+
}
180+
}
181+
182+
public static parsePromptMetadataForVariableDefinitions(text: string | undefined) : PromptVariableDefinition[] {
183+
const varDefs : PromptVariableDefinition[] = [];
184+
const parsedDefs = JSON.parse(text || "[]");
185+
if (Array.isArray(parsedDefs)) {
186+
for (const parsedDef of parsedDefs) {
187+
varDefs.push({
188+
name: parsedDef['name'],
189+
description: parsedDef['description']
190+
});
191+
}
192+
}
193+
194+
return varDefs;
195+
}
196+
197+
public static getDelimitedText(fullText: string, currentLine: number): string | null {
157198
const lines: string[] = fullText.split(Constants.LineSplitterRegex);
158199
const delimiterLineNumbers: number[] = this.getDelimiterRows(lines);
159200
if (delimiterLineNumbers.length === 0) {
@@ -189,4 +230,30 @@ export class Selector {
189230
.filter(([, value]) => /^#{3,}/.test(value))
190231
.map(([index, ]) => +index);
191232
}
233+
234+
private static handlePromptMetadata(metadatas: Map<RequestMetadata, string | undefined> , text: string) {
235+
const promptVarDef = this.getPrompVariableDefinition(text);
236+
if (promptVarDef) {
237+
const varDefs = this.parsePromptMetadataForVariableDefinitions(metadatas.get(RequestMetadata.Prompt));
238+
varDefs.push(promptVarDef);
239+
metadatas.set(RequestMetadata.Prompt, JSON.stringify(varDefs));
240+
}
241+
}
242+
243+
private static async promptForInput(defs: PromptVariableDefinition[]): Promise<Map<string, string> | null> {
244+
const promptVariables = new Map<string, string>();
245+
for (const { name, description } of defs) {
246+
const value = await window.showInputBox({
247+
prompt: `Input value for "${name}"`,
248+
placeHolder: description
249+
});
250+
if (value !== undefined) {
251+
promptVariables.set(name, value);
252+
} else {
253+
return null;
254+
}
255+
}
256+
return promptVariables;
257+
}
258+
192259
}

src/utils/variableProcessor.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,11 @@ export class VariableProcessor {
1616
[EnvironmentVariableProvider.Instance, true],
1717
];
1818

19-
public static async processRawRequest(request: string) {
19+
public static async processRawRequest(request: string, resolvedVariables: Map<string, string> = new Map<string, string>()) {
2020
const variableReferenceRegex = /\{{2}(.+?)\}{2}/g;
2121
let result = '';
2222
let match: RegExpExecArray | null;
2323
let lastIndex = 0;
24-
const resolvedVariables = new Map<string, string>();
2524
variable:
2625
while (match = variableReferenceRegex.exec(request)) {
2726
result += request.substring(lastIndex, match.index);

0 commit comments

Comments
 (0)