fix(core): support ; and :length, and scope requiredness to the expression

Addresses Copilot's round-2 review on #2035. It reported "no new comments"
but carried three *suppressed* ones; all three reproduced against the
pinned SDK.

1. The `;` path-parameter operator is absent from the SDK's operator list,
   so `{;id}` parsed as a variable literally named ";id" and expanded to
   nothing. Added the operator and its named expansion (`;a=1;b=2`).

2. An RFC 6570 prefix modifier was folded into the variable name: `{id:3}`
   yielded a variable called "id:3" and expanded to nothing. Varspecs are
   now parsed properly and the value truncated before encoding. Truncation
   is by code point, since `String.prototype.slice` counts UTF-16 units and
   would split an astral character into a lone surrogate.

   For both of these the wrong URI is the lesser problem: a form has to
   *name* the variables it asks the user to fill, so the panel was
   rendering fields labelled `;id` and `id:3` that nobody could use.

3. Requiredness was applied per variable, but it is a property of the
   *expression*: RFC 6570 drops undefined names from a multi-name
   expression, so `{a,b}` with only `a` filled expands to `a`'s value --
   the SDK does this too. The panel was refusing input the expander would
   have accepted. `hasRequiredValues` now encodes "any one name in a
   required group suffices", and such a field reads "Any one of: a, b"
   rather than falsely claiming each is mandatory.

Two structural consequences, each pinned by a test:

- Takeover is now per TEMPLATE rather than per expression. Splicing
  corrected fragments into a template the SDK re-expands would leave its
  cross-expression `?`-to-`&` rewrite blind to the fragments already
  resolved.

- Expansion is split into a strict variant that throws and a lenient one
  that returns the raw template. `readResourceFromTemplate` wraps the
  thrown error with the template name -- three pre-existing integration
  tests assert that -- while the form must not throw on a server-supplied
  template, since that would take out the panel on render. The strict
  variant constructs the SDK template unconditionally, because that
  construction is what validates syntax: otherwise `x://{;a}{b,c` would
  take the own-expansion path and its unclosed tail would pass as literal
  text with nothing objecting.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-16 19:17:05 -04:00
commit 25c3ad272e

View file

@ -23,12 +23,18 @@ describe("previewUriTemplate", () => {
).toBe("foobar://events/foo%2Fbar");
});
it("keeps a multi-name expression whole until every name is filled", () => {
it("expands a partially-filled multi-name expression, as submitting would", () => {
// RFC 6570 drops the undefined names rather than the whole expression, so
// showing `{?one,two}` here would promise a URI the submit does not send.
expect(previewUriTemplate("x://a{?one,two}", { one: "1" })).toBe(
"x://a{?one,two}",
"x://a?one=1",
);
});
it("keeps a multi-name expression whole while none of its names is filled", () => {
expect(previewUriTemplate("x://a{?one,two}", {})).toBe("x://a{?one,two}");
});
it("still rewrites the second ? to & when both query expressions resolve", () => {
expect(
previewUriTemplate("x://a{?one}{?two}", { one: "1", two: "2" }),