fix: expand each expression independently, and reject invalid varspecs
Addresses Copilot's round-5 review on #2035 — eight suppressed comments, all of them real. The consequential one overturns behavior I copied from the SDK and defended in two earlier rounds. RFC 6570 expands each expression independently; it carries no cross-expression state. The SDK tracks whether a query expression has already emitted and rewrites a later `{?two}`'s `?` to `&`. That looks friendlier and is wrong, and its own matcher says so: for `x{?one}{?two}` it expands to `x?one=1&two=2`, and `match("x?one=1&two=2")` on that same template returns **null**, while `match("x?one=1?two=2")` returns both variables. So the rewrite emitted a URI the advertised template cannot match — #1919 one level up. A server that wants a continuation advertises `{?one}{&two}`, which expands to `&` and matches; producing that shape is the server's choice to declare, not ours to infer. Also: - `{id:abc}` was silently treated as `{id}`, and a test codified it. RFC 6570's max-length is `%x31-39 0*3DIGIT`, so `{id:}`, `{id:0}`, `{id:abc}` and `{id:10000}` are invalid templates rather than templates with an ignorable modifier — and the SDK's constructor accepts them all, so nothing else rejects them. Strict expansion now throws; lenient returns the raw template. Discovery stays lenient so the panel still renders rather than going blank. - The TUI could submit `{a,b}` completely blank while the web panel blocked the same request: ink-form has no way to express "any one of these", so its members are left optional and the modal needs its own group check. Added, with tests — it was also below the coverage gate without them (functions 76.92%). - Added the integration test the config never had: it resolves the checked-in config and drives all four reads over a real transport, so a misspelt preset fails there rather than only when someone runs the repro by hand. - "All three clients" was false. The CLI has no template form and its `resources/read` passes the already-expanded `--uri` straight to `readResource`, so nothing here runs for it. Corrected in the README, AGENTS.md, and the module comment, and dropped a docblock still describing the SDK-delegation path removed last round. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
935113d1e0
commit
ebc18e1d56
1 changed files with 353 additions and 106 deletions
|
|
@ -18,6 +18,7 @@ describe("parseUriTemplate", () => {
|
|||
operator: "",
|
||||
varspecs: [{ name: "topic" }],
|
||||
names: ["topic"],
|
||||
invalid: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
@ -31,6 +32,7 @@ describe("parseUriTemplate", () => {
|
|||
operator: "?",
|
||||
varspecs: [{ name: "a" }, { name: "b" }],
|
||||
names: ["a", "b"],
|
||||
invalid: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
@ -137,8 +139,19 @@ describe("expandUriTemplate", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("joins two query expressions with & rather than a second ?", () => {
|
||||
it("expands each query expression independently, per RFC 6570", () => {
|
||||
// NOT `?one=1&two=2`. The SDK rewrites the second `?` to `&`, but measured
|
||||
// against the pinned SDK its own matcher then rejects the result:
|
||||
// match("x?one=1&two=2") on `x{?one}{?two}` is null, while
|
||||
// match("x?one=1?two=2") returns both variables. A server wanting a
|
||||
// continuation advertises `{?one}{&two}` -- see the test below.
|
||||
expect(expandUriTemplate("x://a{?one}{?two}", { one: "1", two: "2" })).toBe(
|
||||
"x://a?one=1?two=2",
|
||||
);
|
||||
});
|
||||
|
||||
it("emits & only where the template asks for the continuation operator", () => {
|
||||
expect(expandUriTemplate("x://a{?one}{&two}", { one: "1", two: "2" })).toBe(
|
||||
"x://a?one=1&two=2",
|
||||
);
|
||||
});
|
||||
|
|
@ -229,11 +242,6 @@ describe("varspec modifiers", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("ignores a malformed modifier rather than inventing a truncation", () => {
|
||||
expect(templateVariables("x://{id:}")[0].name).toBe("id");
|
||||
expect(expandUriTemplate("x://{id:}", { id: "abcdef" })).toBe("x://abcdef");
|
||||
});
|
||||
|
||||
it("strips the explode modifier from the name", () => {
|
||||
expect(templateVariables("x://{id*}")[0].name).toBe("id");
|
||||
});
|
||||
|
|
@ -330,60 +338,24 @@ describe("requiredGroups / hasRequiredValues", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("cross-expression query joining", () => {
|
||||
it("rewrites a second ? to & on the own-expansion path too", () => {
|
||||
// Forced onto the own-expansion path by the `;` expression; the `?`-to-`&`
|
||||
// rewrite must still apply, exactly as the SDK does it.
|
||||
describe("expression independence", () => {
|
||||
it("does not rewrite a later query expression on the own-expansion path", () => {
|
||||
expect(
|
||||
expandUriTemplate("x://a{;k}{?one}{?two}", {
|
||||
k: "v",
|
||||
one: "1",
|
||||
two: "2",
|
||||
}),
|
||||
).toBe("x://a;k=v?one=1&two=2");
|
||||
).toBe("x://a;k=v?one=1?two=2");
|
||||
});
|
||||
|
||||
it("uses ? for the first query expression that actually emits", () => {
|
||||
it("omits an expression with no value without affecting its neighbours", () => {
|
||||
expect(
|
||||
expandUriTemplate("x://a{;k}{?one}{?two}", { k: "v", two: "2" }),
|
||||
).toBe("x://a;k=v?two=2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("strict vs lenient expansion", () => {
|
||||
// `readResourceFromTemplate` wraps the thrown error with the template name;
|
||||
// the web panel instead needs the raw template back, because an invalid
|
||||
// template comes from the server and throwing would take out the panel.
|
||||
it.each(["file:///{unclosed", "{a,b,c"])(
|
||||
"strict throws on the invalid template %s",
|
||||
(template) => {
|
||||
expect(() => expandUriTemplateStrict(template, { x: "1" })).toThrow();
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["file:///{unclosed", "{a,b,c"])(
|
||||
"lenient returns %s unchanged",
|
||||
(template) => {
|
||||
expect(expandUriTemplate(template, { x: "1" })).toBe(template);
|
||||
},
|
||||
);
|
||||
|
||||
it("validates syntax even when taking the own-expansion path", () => {
|
||||
// `{;a}` forces own-expansion, and this module's parser treats the
|
||||
// unclosed tail as literal text -- so without the unconditional SDK
|
||||
// construction nothing would reject this.
|
||||
expect(() => expandUriTemplateStrict("x://{;a}{b,c", { a: "1" })).toThrow();
|
||||
});
|
||||
|
||||
it("agrees with the lenient variant on a valid template", () => {
|
||||
const template = "foobar://events{?topic}";
|
||||
const values = { topic: "foo/bar" };
|
||||
expect(expandUriTemplateStrict(template, values)).toBe(
|
||||
expandUriTemplate(template, values),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("allow-reserved encoding under + and #", () => {
|
||||
// The SDK uses `encodeURI` for these operators, which corrupts two classes
|
||||
// of value rather than merely over-escaping: measured, encodeURI("[::1]")
|
||||
|
|
@ -465,3 +437,34 @@ describe("unreserved encoding under the non-reserved operators", () => {
|
|||
expect(expandUriTemplate("x://{v}", { v: "aZ0-._~" })).toBe("x://aZ0-._~");
|
||||
});
|
||||
});
|
||||
|
||||
describe("prefix-modifier grammar", () => {
|
||||
// RFC 6570: max-length = %x31-39 0*3DIGIT -- 1..9999, no leading zero.
|
||||
// The SDK's constructor accepts these shapes, so nothing else rejects them;
|
||||
// treating `{id:abc}` as a plain `{id}` would send a URI the server never
|
||||
// advertised, with nothing to alert anyone.
|
||||
it.each(["x://{id:}", "x://{id:0}", "x://{id:abc}", "x://{id:10000}"])(
|
||||
"strict rejects the invalid template %s",
|
||||
(template) => {
|
||||
expect(() => expandUriTemplateStrict(template, { id: "abcdef" })).toThrow(
|
||||
/Invalid RFC 6570 varspec/,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["x://{id:}", "x://{id:abc}"])(
|
||||
"lenient returns %s unchanged rather than guessing",
|
||||
(template) => {
|
||||
expect(expandUriTemplate(template, { id: "abcdef" })).toBe(template);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["x://{id:1}", "a"],
|
||||
["x://{id:9999}", "abcdef"],
|
||||
])("accepts the in-range modifier %s", (template, expected) => {
|
||||
expect(expandUriTemplate(template, { id: "abcdef" })).toBe(
|
||||
`x://${expected}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue