feat(syntax): parse the apis container block

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ClintchizandClaude Opus 5 committed 2026-08-20 02:28:23 +05:30
1 parent d069f5ddd7
commit 87a00de5f3
6 files changed
+222 -5

No files matched your search

+48
View File
@@ -171,3 +171,51 @@ export function parseApiSections(source: string): ApiSections | null {
export function hasRequestSection(source: string): boolean {
return scanTopLevelBlocks(source, SECTION_NAMES).has("request");
}
const emptySections: ApiSections = { parameters: [], body: [], response: "", error: "" };
/** Shape of an entry parsed from an `apis { }` container body. */
export interface ApiEntry {
mode: "any";
name: string;
method: string;
path: string;
body: string;
sections: ApiSections;
}
/**
* Parse the body of a page-level `apis { }` container: a sequence of
* `<name> <METHOD> <path> { ... }` entries with no leading `api` keyword
* (the container supplies it). Each entry's braces are sliced with the
* tokenizer's own `Lexer.readBalancedBraces()`, so the same string- and
* comment-aware rules that protect `parseApiSections` apply here too.
*/
export function parseApiEntries(source: string): ApiEntry[] {
const entries: ApiEntry[] = [];
const lx = new Lexer(source);
while (lx.peek().type !== "eof") {
const nameToken = lx.next();
if (nameToken.type !== "ident") {
throw new LexError(`Expected an api entry name at offset ${nameToken.pos}`);
}
const methodToken = lx.next();
if (methodToken.type !== "ident") {
throw new LexError(`Expected an HTTP method after "${nameToken.value}"`);
}
const path = lx.readPath();
const entryBody = lx.readBalancedBraces();
entries.push({
mode: "any",
name: nameToken.value,
method: methodToken.value.toUpperCase(),
path,
body: "",
sections: parseApiSections(entryBody) ?? emptySections,
});
}
return entries;
}