Reuse tokenizer.readBalancedBraces string/comment skipping (extracted as skipLiteralOrComment) for both section detection and slicing, instead of a second hand-rolled brace counter. Fixes truncation on braces inside strings and false-positive sectioned detection from keywords inside comments/strings. Also switch api-sections.ts errors from plain Error to LexError so parser.ts upgrades them to ParseError with an offset. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
138 lines
3.1 KiB
TypeScript
138 lines
3.1 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { parse } from "../src/index.ts";
|
|
|
|
const page = (inner: string) => `page Repro {
|
|
client {
|
|
${inner}
|
|
}
|
|
|
|
view { <main>x</main> }
|
|
}
|
|
`;
|
|
|
|
test("parses a sectioned api block into request, response and error", () => {
|
|
const ast = parse(
|
|
page(` api searchUsers POST /api/users {
|
|
request {
|
|
body {
|
|
name?: string
|
|
age?: number
|
|
}
|
|
}
|
|
|
|
response {
|
|
return data.users
|
|
}
|
|
|
|
error {
|
|
return []
|
|
}
|
|
}`),
|
|
);
|
|
|
|
const block = ast.dataApis[0]!;
|
|
expect(block.name).toBe("searchUsers");
|
|
expect(block.method).toBe("POST");
|
|
expect(block.path).toBe("/api/users");
|
|
expect(block.sections?.body).toEqual([
|
|
{ name: "name", optional: true, type: "string" },
|
|
{ name: "age", optional: true, type: "number" },
|
|
]);
|
|
expect(block.sections?.response.trim()).toBe("return data.users");
|
|
expect(block.sections?.error.trim()).toBe("return []");
|
|
});
|
|
|
|
test("a bare body still parses as the legacy response body", () => {
|
|
const ast = parse(
|
|
page(` api legacyUsers GET /api/users {
|
|
return users.length
|
|
}`),
|
|
);
|
|
|
|
const block = ast.dataApis[0]!;
|
|
expect(block.sections).toBeUndefined();
|
|
expect(block.body.trim()).toBe("return users.length");
|
|
});
|
|
|
|
test("GET parameters are parsed as required when not marked optional", () => {
|
|
const ast = parse(
|
|
page(` api listUsers GET /api/users {
|
|
request {
|
|
parameters {
|
|
team: string
|
|
}
|
|
}
|
|
|
|
response {
|
|
return data.users
|
|
}
|
|
}`),
|
|
);
|
|
|
|
expect(ast.dataApis[0]!.sections?.parameters).toEqual([
|
|
{ name: "team", optional: false, type: "string" },
|
|
]);
|
|
});
|
|
|
|
test("request inside an ssr block is rejected with a message naming the restriction", () => {
|
|
const source = `page Repro {
|
|
ssr {
|
|
api ssrUsers GET /api/users {
|
|
request {
|
|
parameters {
|
|
team: string
|
|
}
|
|
}
|
|
|
|
response {
|
|
return data.users
|
|
}
|
|
}
|
|
}
|
|
|
|
view { <main>x</main> }
|
|
}
|
|
`;
|
|
|
|
expect(() => parse(source)).toThrow(/ssr[\s\S]*request/i);
|
|
});
|
|
|
|
test("a brace inside a string literal in the response body does not truncate the section", () => {
|
|
const ast = parse(
|
|
page(` api searchUsers POST /api/users {
|
|
request {
|
|
body {
|
|
name?: string
|
|
}
|
|
}
|
|
|
|
response {
|
|
return "a } weird string"
|
|
}
|
|
|
|
error {
|
|
return []
|
|
}
|
|
}`),
|
|
);
|
|
|
|
const block = ast.dataApis[0]!;
|
|
expect(block.sections?.response.trim()).toBe('return "a } weird string"');
|
|
expect(block.sections?.error.trim()).toBe("return []");
|
|
});
|
|
|
|
test("a legacy block whose comment or string mentions a section keyword stays legacy", () => {
|
|
const ast = parse(
|
|
page(` api legacyUsers GET /api/users {
|
|
// fall back to a manual request { } if this fails
|
|
return "response { not a section }"
|
|
}`),
|
|
);
|
|
|
|
const block = ast.dataApis[0]!;
|
|
expect(block.sections).toBeUndefined();
|
|
expect(block.body.trim()).toBe(
|
|
'// fall back to a manual request { } if this fails\n return "response { not a section }"',
|
|
);
|
|
});
|