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;
|
||||
}
|
||||
Reference in New Issue
Block a user