Makros: GET/POST-Anfragen mit JSON-Ergebnis und FOR-Iteration
Erweitert die Makro-Sprache um Netzwerkzugriff: "x = GET(url)"/ "x = POST(url; body)" binden die geparste JSON-Antwort an eine Variable; FOR item IN x DO ... END iteriert, wenn x ein JSON-Array ist, über dessen Elemente; JSONGET(wert; pfad) liest ein Feld heraus (punktgetrennt, Zahl = Array-Index) und funktioniert – anders als GET/POST – überall in einem Ausdruck verschachtelt, nicht nur als ganze Zuweisung. Da ein echter Netzwerkaufruf nicht synchron laufen kann, ohne den Tab einzufrieren, bekommt der Makro-Interpreter einen separaten asynchronen Zweig (execBlockAsync/execStatementAsync); der bestehende synchrone Interpreter, den auch gewöhnliche "Mit Schleife"-Funktionen mitten in der synchronen Neuberechnung nutzen, bleibt unangetastet. Netzwerkaufrufe werden pro Makro einmalig mit Zielname bestätigt (dauerhaft gemerkt); die bestehende "ein Lauf = ein Undo-Schritt"-Garantie, die bislang auf Synchronität beruhte, wird jetzt explizit wiederhergestellt statt sich weiter auf die Zeit-Koaleszenz zu verlassen. Zwei echte Lücken erst beim eigenen Live-Test gefunden und behoben: (1) JSONGET direkt in eine Aktion verschachtelt (statt erst einer Variable zugewiesen) scheiterte zunächst an #NAME?; (2) JSONGET(...) direkt als FOR-Quelle (ohne Zwischenvariable) scheiterte sowohl an der Speicher-Zeit-Prüfung als auch zur Laufzeit, da substitute() einen Funktionsaufruf nie auswertet. Live gegen eine neue same-origin-Fixture (public/test-fixtures/sample.json) verifiziert: echter GET, echter Bestätigungsdialog, FOR/JSONGET über die echte Antwort, kein erneuter Dialog beim zweiten Lauf. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -115,6 +115,7 @@ function expand(
|
||||
case 'namedref':
|
||||
case 'param':
|
||||
case 'omitted':
|
||||
case 'json':
|
||||
return node; // leaves -- nothing to expand
|
||||
case 'unary':
|
||||
return { ...node, operand: expand(node.operand, index, bodyCache, blockCache, depth) };
|
||||
@@ -203,6 +204,7 @@ export function substitute(node: Node, bound: ReadonlyMap<string, Node>): Node {
|
||||
case 'range':
|
||||
case 'namedref':
|
||||
case 'omitted':
|
||||
case 'json':
|
||||
return node;
|
||||
case 'unary':
|
||||
return { ...node, operand: substitute(node.operand, bound) };
|
||||
@@ -328,7 +330,7 @@ export function declaredBlockNames(block: BlockNode, out: string[] = []): string
|
||||
export function findInvalidForSource(block: BlockNode): ForNode | null {
|
||||
for (const stmt of block.statements) {
|
||||
if (stmt.kind === 'for') {
|
||||
if (!isReferenceShaped(stmt.source)) return stmt;
|
||||
if (!isReferenceShaped(stmt.source) && !isJsonGetCall(stmt.source)) return stmt;
|
||||
const nested = findInvalidForSource(stmt.body);
|
||||
if (nested) return nested;
|
||||
} else if (stmt.kind === 'ifstmt') {
|
||||
@@ -348,6 +350,17 @@ function isReferenceShaped(node: Node): boolean {
|
||||
return node.kind === 'param' || node.kind === 'cell' || node.kind === 'range' || node.kind === 'namedref';
|
||||
}
|
||||
|
||||
/** A `FOR` source may also be a `JSONGET(...)` call -- once substituted and
|
||||
* evaluated it may resolve to a JSON array (see evaluator.ts's
|
||||
* `execStatementAsync` `'for'` case), a possibility `isReferenceShaped`
|
||||
* above doesn't cover since it's a genuinely new FOR-source *kind*, not a
|
||||
* reference. Not shared with `findInvalidActionTarget`'s SETVALUE-target
|
||||
* check -- a JSONGET result is a VALUE, never a place to write to, so a
|
||||
* write target must stay reference-shaped only. */
|
||||
function isJsonGetCall(node: Node): boolean {
|
||||
return node.kind === 'func' && node.name.toUpperCase() === 'JSONGET';
|
||||
}
|
||||
|
||||
/** {@link findInvalidForSource}'s counterpart for a macro's `SETVALUE`
|
||||
* target (its first argument) instead of a FOR source -- same reasoning,
|
||||
* checked at save time by store/document.ts#validateMacro. */
|
||||
|
||||
Reference in New Issue
Block a user