Print template strings in plain quotes unless they can't round-trip (#708)

This commit is contained in:
Gregory Schier authored and GitHub committed 2026-09-25 09:07:28 -07:00
1 parent 6e7f23a778
commit 23d369d84e
6 files changed
+191 -19

No files matched your search

+1 -10
View File
@@ -9,6 +9,7 @@ import type { HeaderValuePreset } from "../lib/data/headerValuePresets";
import { headerNames } from "../lib/data/headerNames";
import { mimeTypes } from "../lib/data/mimetypes";
import { userAgents } from "../lib/data/userAgents";
import { validateHttpHeader } from "../lib/validateHttpHeader";
import { CountBadge } from "./core/CountBadge";
import { DetailsBanner } from "./core/DetailsBanner";
import type { GenericCompletion, GenericCompletionConfig } from "./core/Editor/genericCompletion";
@@ -178,13 +179,3 @@ const nameAutocomplete: PairEditorProps["nameAutocomplete"] = {
},
),
};
const validateHttpHeader = (v: string) => {
if (v === "") {
return true;
}
// Template strings are not allowed so we replace them with a valid example string
const withoutTemplateStrings = v.replace(/\$\{\[\s*[^\]\s]+\s*]}/gi, "123");
return withoutTemplateStrings.match(/^[a-zA-Z0-9-_]+$/) !== null;
};
@@ -0,0 +1,51 @@
import type { HttpRequest } from "@yaakapp-internal/models";
import { describe, expect, test, vi } from "vite-plus/test";
// resolvedModelName only reads plain model fields, but its module imports pull in the
// platform singleton, which needs a browser. Stub them out to keep this a pure unit test.
vi.mock("@yaakapp-internal/models", () => ({ foldersAtom: {} }));
vi.mock("./jotai", () => ({ jotaiStore: { get: () => [] } }));
const { resolvedModelName } = await import("./resolvedModelName");
function httpRequest(url: string): HttpRequest {
return { id: "rq_test", model: "http_request", name: "", url } as HttpRequest;
}
describe("resolvedModelName", () => {
test("returns the name when there is one", () => {
expect(
resolvedModelName({ ...httpRequest("https://example.com"), name: "My Request" }),
).toEqual("My Request");
});
test("replaces a simple tag with its contents", () => {
expect(resolvedModelName(httpRequest("${[ base_url ]}/users"))).toEqual("base_url/users");
});
test("replaces a tag containing a quoted argument with a space", () => {
expect(resolvedModelName(httpRequest("${[ fn(arg='my key') ]}/users"))).toEqual(
"fn(arg='my key')/users",
);
});
test("replaces each tag in a multi-tag string", () => {
expect(
resolvedModelName(httpRequest("${[ fn(arg='my key') ]}/a/${[ other(b='x y') ]}/b")),
).toEqual("fn(arg='my key')/a/other(b='x y')/b");
});
test("stops each tag at its first closing bracket", () => {
expect(resolvedModelName(httpRequest("${[ a ]}${[ b ]}"))).toEqual("ab");
});
test("strips the protocol", () => {
expect(resolvedModelName(httpRequest("https://example.com/${[ path ]}"))).toEqual(
"example.com/path",
);
});
test("falls back to a generic name when the url is empty", () => {
expect(resolvedModelName(httpRequest(""))).toEqual("HTTP Request");
});
});
+8 -1
View File
@@ -2,6 +2,13 @@ import type { AnyModel } from "@yaakapp-internal/models";
import { foldersAtom } from "@yaakapp-internal/models";
import { jotaiStore } from "./jotai";
/**
* Matches a template tag up to the first `]}`, the same way the Lezer grammar and the JSON
* linter close a tag. Quoted arguments may contain spaces, so this can't stop at whitespace.
* The capture group holds the tag's inner content, trimmed of the padding spaces.
*/
const TEMPLATE_TAG_REGEX = /\$\{\[\s*([\s\S]*?)\s*]}/g;
export function resolvedModelName(r: AnyModel | null): string {
if (r == null) return "";
@@ -15,7 +22,7 @@ export function resolvedModelName(r: AnyModel | null): string {
}
// Replace variable syntax with variable name
const withoutVariables = r.url.replace(/\$\{\[\s*([^\]\s]+)\s*]}/g, "$1");
const withoutVariables = r.url.replace(TEMPLATE_TAG_REGEX, "$1");
if (withoutVariables.trim() === "") {
return r.model === "http_request"
? r.bodyType && r.bodyType === "graphql"
@@ -0,0 +1,32 @@
import { describe, expect, test } from "vite-plus/test";
import { validateHttpHeader } from "./validateHttpHeader";
describe("validateHttpHeader", () => {
test("allows an empty value", () => {
expect(validateHttpHeader("")).toEqual(true);
});
test("allows a plain header name", () => {
expect(validateHttpHeader("X-Api-Key")).toEqual(true);
});
test("rejects a header name with a space", () => {
expect(validateHttpHeader("X Api Key")).toEqual(false);
});
test("allows a simple template tag", () => {
expect(validateHttpHeader("${[ my_var ]}")).toEqual(true);
});
test("allows a tag containing a quoted argument with a space", () => {
expect(validateHttpHeader("${[ fn(arg='my key') ]}")).toEqual(true);
});
test("allows a multi-tag value", () => {
expect(validateHttpHeader("${[ fn(arg='my key') ]}-${[ other(b='x y') ]}")).toEqual(true);
});
test("rejects a tag followed by an invalid character", () => {
expect(validateHttpHeader("${[ my_var ]} oops")).toEqual(false);
});
});
@@ -0,0 +1,15 @@
/**
* Matches a template tag up to the first `]}`, the same way the Lezer grammar and the JSON
* linter close a tag. Quoted arguments may contain spaces, so this can't stop at whitespace.
*/
const TEMPLATE_TAG_REGEX = /\$\{\[[\s\S]*?]}/g;
export function validateHttpHeader(v: string): boolean {
if (v === "") {
return true;
}
// Template strings are not allowed so we replace them with a valid example string
const withoutTemplateStrings = v.replace(TEMPLATE_TAG_REGEX, "123");
return withoutTemplateStrings.match(/^[a-zA-Z0-9-_]+$/) !== null;
}