feat(syntax): parse sectioned api blocks
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Parse the sectioned form of an `api` block body.
|
||||
*
|
||||
* Returns null when no section keyword is present, which is how the legacy
|
||||
* bare-body form stays valid: the caller keeps treating the body as the
|
||||
* response expression.
|
||||
*/
|
||||
export interface ApiFieldDecl {
|
||||
name: string;
|
||||
optional: boolean;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface ApiSections {
|
||||
parameters: ApiFieldDecl[];
|
||||
body: ApiFieldDecl[];
|
||||
response: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
const SECTION_NAMES = ["request", "response", "error"] as const;
|
||||
|
||||
/** Slice the balanced `{ ... }` that follows `keyword`, or null when absent. */
|
||||
function sectionBody(source: string, keyword: string): string | null {
|
||||
const match = new RegExp(`(^|[^A-Za-z0-9_$])${keyword}\\s*\\{`).exec(source);
|
||||
if (!match) return null;
|
||||
|
||||
const open = source.indexOf("{", match.index + match[1]!.length);
|
||||
let depth = 0;
|
||||
|
||||
for (let index = open; index < source.length; index++) {
|
||||
const character = source[index];
|
||||
if (character === "{") depth++;
|
||||
else if (character === "}") {
|
||||
depth--;
|
||||
if (depth === 0) return source.slice(open + 1, index);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unclosed "${keyword}" section in an api block`);
|
||||
}
|
||||
|
||||
/** `name?: string` -> { name, optional, type }. Blank lines and comments are skipped. */
|
||||
function parseFields(source: string): ApiFieldDecl[] {
|
||||
const fields: ApiFieldDecl[] = [];
|
||||
|
||||
for (const rawLine of source.split("\n")) {
|
||||
const line = rawLine.trim().replace(/,$/, "");
|
||||
if (!line || line.startsWith("//")) continue;
|
||||
|
||||
const match = /^([A-Za-z_$][A-Za-z0-9_$]*)(\?)?\s*:\s*(.+)$/.exec(line);
|
||||
if (!match) {
|
||||
throw new Error(`Expected "name: type" in an api request section, got "${line}"`);
|
||||
}
|
||||
|
||||
fields.push({ name: match[1]!, optional: match[2] === "?", type: match[3]!.trim() });
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
export function parseApiSections(source: string): ApiSections | null {
|
||||
const present = SECTION_NAMES.some((name) => sectionBody(source, name) !== null);
|
||||
if (!present) return null;
|
||||
|
||||
const request = sectionBody(source, "request");
|
||||
|
||||
return {
|
||||
parameters: request ? parseFields(sectionBody(request, "parameters") ?? "") : [],
|
||||
body: request ? parseFields(sectionBody(request, "body") ?? "") : [],
|
||||
response: sectionBody(source, "response") ?? "",
|
||||
error: sectionBody(source, "error") ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
/** True when the block declares a `request` section. */
|
||||
export function hasRequestSection(source: string): boolean {
|
||||
return sectionBody(source, "request") !== null;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { WRN_RUNTIME_TARGETS } from "./spec.ts";
|
||||
import { parseApiSections, hasRequestSection, type ApiSections } from "./api-sections.ts";
|
||||
|
||||
/**
|
||||
* Recursive-descent parser for `.wrn`, producing a small AST.
|
||||
@@ -148,7 +149,10 @@ export interface DataApiBlock {
|
||||
name: string;
|
||||
method: string;
|
||||
path: string;
|
||||
/** Legacy bare body. Empty string when `sections` is set. */
|
||||
body: string;
|
||||
/** Present only for the sectioned, typed form. */
|
||||
sections?: ApiSections;
|
||||
}
|
||||
|
||||
export interface ModeFunctionsBlock {
|
||||
@@ -696,7 +700,20 @@ export function parse(source: string): PageAst {
|
||||
const method = expect("ident").value.toUpperCase();
|
||||
const path = lx.readPath();
|
||||
const body = lx.readBalancedBraces();
|
||||
dataApis.push({ mode, name, method, path, body });
|
||||
const sections = parseApiSections(body);
|
||||
if (sections && mode !== "client" && hasRequestSection(body)) {
|
||||
throw new ParseError(
|
||||
`An ssr api block cannot declare "request": there is no caller at render time to supply it. Use a client block, or a server function.`,
|
||||
);
|
||||
}
|
||||
dataApis.push({
|
||||
mode,
|
||||
name,
|
||||
method,
|
||||
path,
|
||||
body: sections ? "" : body,
|
||||
...(sections ? { sections } : {}),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "functions": {
|
||||
|
||||
Reference in New Issue
Block a user