@kernlang/python 3.5.4-canary.157.1.43256f52 → 3.5.4-canary.161.2.d4dbfea4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fastapi-portable.d.ts +35 -2
- package/dist/fastapi-portable.js +216 -25
- package/dist/fastapi-portable.js.map +1 -1
- package/dist/fastapi-response.d.ts +1 -1
- package/dist/fastapi-response.js +604 -4
- package/dist/fastapi-response.js.map +1 -1
- package/dist/fastapi-route.d.ts +14 -2
- package/dist/fastapi-route.js +117 -68
- package/dist/fastapi-route.js.map +1 -1
- package/dist/fastapi-utils.d.ts +2 -0
- package/dist/fastapi-utils.js +90 -0
- package/dist/fastapi-utils.js.map +1 -1
- package/dist/transpiler-fastapi.js +10 -2
- package/dist/transpiler-fastapi.js.map +1 -1
- package/package.json +2 -2
package/dist/fastapi-response.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* addRespondImports — add necessary imports for respond node
|
|
8
8
|
*/
|
|
9
9
|
import { getProps } from '@kernlang/core';
|
|
10
|
-
import { escapePyStr } from './fastapi-utils.js';
|
|
10
|
+
import { escapePyStr, quoteObjectKeysOutsideStrings } from './fastapi-utils.js';
|
|
11
11
|
import { toSnakeCase } from './type-map.js';
|
|
12
12
|
export function generateRespondFastAPI(respondNode, indent) {
|
|
13
13
|
const p = getProps(respondNode);
|
|
@@ -87,15 +87,584 @@ function lowerJsArrayMethods(expr) {
|
|
|
87
87
|
}
|
|
88
88
|
return next;
|
|
89
89
|
}
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
// Index of the bracket that closes the one at `openIdx`, tracking ()[]{} depth
|
|
91
|
+
// and skipping string/template literals. -1 if unbalanced.
|
|
92
|
+
function matchBalancedParen(expr, openIdx) {
|
|
93
|
+
let depth = 0;
|
|
94
|
+
let quote = null;
|
|
95
|
+
for (let i = openIdx; i < expr.length; i++) {
|
|
96
|
+
const c = expr[i];
|
|
97
|
+
if (quote) {
|
|
98
|
+
if (c === '\\')
|
|
99
|
+
i += 1;
|
|
100
|
+
else if (c === quote)
|
|
101
|
+
quote = null;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (c === '"' || c === "'" || c === '`')
|
|
105
|
+
quote = c;
|
|
106
|
+
else if (c === '(' || c === '[' || c === '{')
|
|
107
|
+
depth += 1;
|
|
108
|
+
else if (c === ')' || c === ']' || c === '}') {
|
|
109
|
+
depth -= 1;
|
|
110
|
+
if (depth === 0)
|
|
111
|
+
return i;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return -1;
|
|
115
|
+
}
|
|
116
|
+
// Split a call's inner argument text on top-level commas, ignoring commas
|
|
117
|
+
// inside nested ()[]{} or string literals.
|
|
118
|
+
function splitTopLevelArgs(inner) {
|
|
119
|
+
const args = [];
|
|
120
|
+
let depth = 0;
|
|
121
|
+
let quote = null;
|
|
122
|
+
let start = 0;
|
|
123
|
+
for (let i = 0; i < inner.length; i++) {
|
|
124
|
+
const c = inner[i];
|
|
125
|
+
if (quote) {
|
|
126
|
+
if (c === '\\')
|
|
127
|
+
i += 1;
|
|
128
|
+
else if (c === quote)
|
|
129
|
+
quote = null;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (c === '"' || c === "'" || c === '`')
|
|
133
|
+
quote = c;
|
|
134
|
+
else if (c === '(' || c === '[' || c === '{')
|
|
135
|
+
depth += 1;
|
|
136
|
+
else if (c === ')' || c === ']' || c === '}')
|
|
137
|
+
depth -= 1;
|
|
138
|
+
else if (c === ',' && depth === 0) {
|
|
139
|
+
args.push(inner.slice(start, i).trim());
|
|
140
|
+
start = i + 1;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
args.push(inner.slice(start).trim());
|
|
144
|
+
return args;
|
|
145
|
+
}
|
|
146
|
+
// Lower JSON.stringify(...) / JSON.parse(...) to json.dumps/loads. Uses a
|
|
147
|
+
// balanced, string-aware scan because the single argument can itself contain
|
|
148
|
+
// commas, nested parens, brackets, braces, or string literals — which regex
|
|
149
|
+
// cannot reliably capture (three regex iterations were each holed by review).
|
|
150
|
+
// Skips occurrences inside string literals and those that are a property of
|
|
151
|
+
// another receiver (e.g. `myJSON.stringify`). Handles the pretty-print form
|
|
152
|
+
// `JSON.stringify(x, null, n)` → `json.dumps(x, indent=n)`.
|
|
153
|
+
function lowerJsonBuiltinCalls(expr, imports) {
|
|
154
|
+
let out = '';
|
|
155
|
+
let i = 0;
|
|
156
|
+
let quote = null;
|
|
157
|
+
while (i < expr.length) {
|
|
158
|
+
const c = expr[i];
|
|
159
|
+
if (quote) {
|
|
160
|
+
out += c;
|
|
161
|
+
if (c === '\\') {
|
|
162
|
+
out += expr[i + 1] ?? '';
|
|
163
|
+
i += 2;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (c === quote)
|
|
167
|
+
quote = null;
|
|
168
|
+
i += 1;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
172
|
+
quote = c;
|
|
173
|
+
out += c;
|
|
174
|
+
i += 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const m = expr.slice(i).match(/^JSON\.(stringify|parse)\(/);
|
|
178
|
+
const prev = expr[i - 1];
|
|
179
|
+
if (m && !(prev && /[\w.]/.test(prev))) {
|
|
180
|
+
const method = m[1];
|
|
181
|
+
const openIdx = i + m[0].length - 1;
|
|
182
|
+
const closeIdx = matchBalancedParen(expr, openIdx);
|
|
183
|
+
if (closeIdx !== -1) {
|
|
184
|
+
const args = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx));
|
|
185
|
+
// Recurse so a nested builtin in the argument is lowered too, e.g.
|
|
186
|
+
// JSON.stringify(JSON.parse(x)) → json.dumps(json.loads(x)) (Codex
|
|
187
|
+
// review on 9d8ed8d0). Terminates: the argument is strictly shorter.
|
|
188
|
+
const a0 = lowerJsonBuiltinCalls(args[0] ?? '', imports);
|
|
189
|
+
imports?.add('import json');
|
|
190
|
+
if (method === 'parse') {
|
|
191
|
+
out += `json.loads(${a0})`;
|
|
192
|
+
}
|
|
193
|
+
else if (args.length >= 3 && /^(None|null)$/.test(args[1]) && /^\d+$/.test(args[2])) {
|
|
194
|
+
out += `json.dumps(${a0}, indent=${args[2]})`;
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
out += `json.dumps(${a0})`;
|
|
198
|
+
}
|
|
199
|
+
i = closeIdx + 1;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
out += c;
|
|
204
|
+
i += 1;
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
// Build the Python comprehension for one `Array.from(...)` call's argument list,
|
|
209
|
+
// or return null if the call isn't a lowerable length-form. Uses the balanced
|
|
210
|
+
// helpers (not regex) so a length value or arrow params containing braces/parens
|
|
211
|
+
// don't desync (codex/gemini review of cd7c40ae).
|
|
212
|
+
function tryLowerArrayFrom(args) {
|
|
213
|
+
if (args.length < 2)
|
|
214
|
+
return null;
|
|
215
|
+
// arg0 must be an object literal whose `length` property gives the count.
|
|
216
|
+
const arg0 = args[0].trim();
|
|
217
|
+
if (!arg0.startsWith('{') || matchBalancedParen(arg0, 0) !== arg0.length - 1)
|
|
218
|
+
return null;
|
|
219
|
+
let count = null;
|
|
220
|
+
for (const prop of splitTopLevelArgs(arg0.slice(1, -1))) {
|
|
221
|
+
const mm = prop.match(/^(?:length|["']length["'])\s*:\s*([\s\S]+)$/);
|
|
222
|
+
if (mm) {
|
|
223
|
+
count = mm[1].trim();
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (count === null)
|
|
228
|
+
return null;
|
|
229
|
+
// arg1 must be an arrow `(params) => body` or `param => body`.
|
|
230
|
+
const arrowStr = args[1].trim();
|
|
231
|
+
let params;
|
|
232
|
+
let body;
|
|
233
|
+
if (arrowStr.startsWith('(')) {
|
|
234
|
+
const pClose = matchBalancedParen(arrowStr, 0);
|
|
235
|
+
if (pClose === -1)
|
|
236
|
+
return null;
|
|
237
|
+
const after = arrowStr.slice(pClose + 1).trim();
|
|
238
|
+
if (!after.startsWith('=>'))
|
|
239
|
+
return null;
|
|
240
|
+
params = splitTopLevelArgs(arrowStr.slice(1, pClose))
|
|
241
|
+
.map((s) => s.trim())
|
|
242
|
+
.filter(Boolean);
|
|
243
|
+
body = after.slice(2).trim();
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
const am = arrowStr.match(/^([A-Za-z_$][\w$]*)\s*=>\s*([\s\S]+)$/);
|
|
247
|
+
if (!am)
|
|
248
|
+
return null;
|
|
249
|
+
params = [am[1]];
|
|
250
|
+
body = am[2].trim();
|
|
251
|
+
}
|
|
252
|
+
// Loop var = the INDEX (2nd param). The 1st param is the element, which is
|
|
253
|
+
// undefined for the length form, so it is NOT promoted to the loop variable
|
|
254
|
+
// (doing so would diverge from JS — `(x) => x` is [undefined…], not [0,1,…]).
|
|
255
|
+
// A non-simple index (destructuring) isn't a valid Python loop target → bail.
|
|
256
|
+
const idxVar = params[1] || '_';
|
|
257
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(idxVar))
|
|
258
|
+
return null;
|
|
259
|
+
// `(_, i) => ({...})` parenthesizes the object body to disambiguate it from a
|
|
260
|
+
// block; unwrap ONLY when the enclosed body is an object literal, so a comma
|
|
261
|
+
// operator `(1, 2)` or grouped expr isn't mis-stripped (codex review).
|
|
262
|
+
if (body.startsWith('(') && matchBalancedParen(body, 0) === body.length - 1) {
|
|
263
|
+
const inner = body.slice(1, -1).trim();
|
|
264
|
+
if (inner.startsWith('{'))
|
|
265
|
+
body = inner;
|
|
266
|
+
}
|
|
267
|
+
// Recurse so a nested Array.from in the count or body is lowered too.
|
|
268
|
+
return `[${lowerArrayFromCalls(body)} for ${idxVar} in range(${lowerArrayFromCalls(count)})]`;
|
|
269
|
+
}
|
|
270
|
+
// Expand JS object-literal shorthand properties to explicit `key: key` so the
|
|
271
|
+
// dict-key quoting pass can quote them: `{ items, page }` → `{ items: items,
|
|
272
|
+
// page: page }`. Bracket/string-aware: only an object-literal entry that is a
|
|
273
|
+
// bare identifier is expanded; `key: value`, `**spread`, computed keys, and
|
|
274
|
+
// array/comprehension contents (`[]`) are left alone, and nested objects are
|
|
275
|
+
// handled by recursing into each entry. Runs just before key quoting.
|
|
276
|
+
function expandObjectShorthand(expr) {
|
|
277
|
+
let out = '';
|
|
278
|
+
let i = 0;
|
|
279
|
+
let quote = null;
|
|
280
|
+
while (i < expr.length) {
|
|
281
|
+
const c = expr[i];
|
|
282
|
+
if (quote) {
|
|
283
|
+
out += c;
|
|
284
|
+
if (c === '\\') {
|
|
285
|
+
out += expr[i + 1] ?? '';
|
|
286
|
+
i += 2;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (c === quote)
|
|
290
|
+
quote = null;
|
|
291
|
+
i += 1;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
295
|
+
quote = c;
|
|
296
|
+
out += c;
|
|
297
|
+
i += 1;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (c === '{') {
|
|
301
|
+
const close = matchBalancedParen(expr, i);
|
|
302
|
+
if (close !== -1) {
|
|
303
|
+
const rebuilt = splitTopLevelArgs(expr.slice(i + 1, close)).map((entry) => {
|
|
304
|
+
const t = entry.trim();
|
|
305
|
+
if (t === '')
|
|
306
|
+
return entry;
|
|
307
|
+
if (/^[A-Za-z_$][\w$]*$/.test(t))
|
|
308
|
+
return `${t}: ${t}`;
|
|
309
|
+
return expandObjectShorthand(entry);
|
|
310
|
+
});
|
|
311
|
+
out += `{${rebuilt.join(', ')}}`;
|
|
312
|
+
i = close + 1;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
out += c;
|
|
317
|
+
i += 1;
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
// Lower `Array.from({ length: N }, (_, i) => BODY)` to a Python list
|
|
322
|
+
// comprehension `[BODY for i in range(N)]` (Express keeps Array.from — valid
|
|
323
|
+
// JS). Balanced, string-aware scan; runs BEFORE the ref/key/template passes so
|
|
324
|
+
// they lower N and BODY in place. Only the `{ length: N }` form is handled;
|
|
325
|
+
// `Array.from(iterable, fn)` (map form) is left untouched. A call immediately
|
|
326
|
+
// followed by a method chain (`.map`, `.filter`, …) is left raw rather than
|
|
327
|
+
// lowered, because the array-method pass cannot consume a comprehension
|
|
328
|
+
// receiver and would emit malformed Python (codex review of cd7c40ae).
|
|
329
|
+
function lowerArrayFromCalls(expr) {
|
|
330
|
+
let out = '';
|
|
331
|
+
let i = 0;
|
|
332
|
+
let quote = null;
|
|
333
|
+
while (i < expr.length) {
|
|
334
|
+
const c = expr[i];
|
|
335
|
+
if (quote) {
|
|
336
|
+
out += c;
|
|
337
|
+
if (c === '\\') {
|
|
338
|
+
out += expr[i + 1] ?? '';
|
|
339
|
+
i += 2;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (c === quote)
|
|
343
|
+
quote = null;
|
|
344
|
+
i += 1;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
348
|
+
quote = c;
|
|
349
|
+
out += c;
|
|
350
|
+
i += 1;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const m = expr.slice(i).match(/^Array\.from\(/);
|
|
354
|
+
const prev = expr[i - 1];
|
|
355
|
+
if (m && !(prev && /[\w.]/.test(prev))) {
|
|
356
|
+
const openIdx = i + m[0].length - 1;
|
|
357
|
+
const closeIdx = matchBalancedParen(expr, openIdx);
|
|
358
|
+
if (closeIdx !== -1 && expr[closeIdx + 1] !== '.') {
|
|
359
|
+
const lowered = tryLowerArrayFrom(splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx)));
|
|
360
|
+
if (lowered !== null) {
|
|
361
|
+
out += lowered;
|
|
362
|
+
i = closeIdx + 1;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
out += c;
|
|
368
|
+
i += 1;
|
|
369
|
+
}
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
372
|
+
function scanQuotedString(expr, startIndex, quote) {
|
|
373
|
+
for (let i = startIndex + 1; i < expr.length; i++) {
|
|
374
|
+
if (expr[i] === '\\') {
|
|
375
|
+
i += 1;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (expr[i] === quote)
|
|
379
|
+
return i;
|
|
380
|
+
}
|
|
381
|
+
return -1;
|
|
382
|
+
}
|
|
383
|
+
function scanTemplateInterpolationEnd(expr, startIndex) {
|
|
384
|
+
let depth = 1;
|
|
385
|
+
for (let i = startIndex; i < expr.length; i++) {
|
|
386
|
+
const c = expr[i];
|
|
387
|
+
if (c === '"' || c === "'") {
|
|
388
|
+
const quotedEnd = scanQuotedString(expr, i, c);
|
|
389
|
+
if (quotedEnd === -1)
|
|
390
|
+
return -1;
|
|
391
|
+
i = quotedEnd;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (c === '`') {
|
|
395
|
+
const templateEnd = scanTemplateLiteralEnd(expr, i);
|
|
396
|
+
if (templateEnd === -1)
|
|
397
|
+
return -1;
|
|
398
|
+
i = templateEnd;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (c === '{')
|
|
402
|
+
depth += 1;
|
|
403
|
+
else if (c === '}') {
|
|
404
|
+
depth -= 1;
|
|
405
|
+
if (depth === 0)
|
|
406
|
+
return i;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return -1;
|
|
410
|
+
}
|
|
411
|
+
function scanTemplateLiteralEnd(expr, startIndex) {
|
|
412
|
+
for (let i = startIndex + 1; i < expr.length; i++) {
|
|
413
|
+
const c = expr[i];
|
|
414
|
+
if (c === '\\') {
|
|
415
|
+
i += 1;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (c === '`')
|
|
419
|
+
return i;
|
|
420
|
+
if (c === '$' && expr[i + 1] === '{') {
|
|
421
|
+
const interpolationEnd = scanTemplateInterpolationEnd(expr, i + 2);
|
|
422
|
+
if (interpolationEnd === -1)
|
|
423
|
+
return -1;
|
|
424
|
+
i = interpolationEnd;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return -1;
|
|
428
|
+
}
|
|
429
|
+
function parseTemplateLiteral(expr, startIndex) {
|
|
430
|
+
const textParts = [];
|
|
431
|
+
const interpolationParts = [];
|
|
432
|
+
let text = '';
|
|
433
|
+
for (let i = startIndex + 1; i < expr.length;) {
|
|
434
|
+
const c = expr[i];
|
|
435
|
+
if (c === '\\') {
|
|
436
|
+
text += c;
|
|
437
|
+
if (i + 1 < expr.length)
|
|
438
|
+
text += expr[i + 1];
|
|
439
|
+
i += 2;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (c === '`') {
|
|
443
|
+
textParts.push(text);
|
|
444
|
+
return { endIndex: i, textParts, interpolationParts };
|
|
445
|
+
}
|
|
446
|
+
if (c === '$' && expr[i + 1] === '{') {
|
|
447
|
+
textParts.push(text);
|
|
448
|
+
text = '';
|
|
449
|
+
const interpolationEnd = scanTemplateInterpolationEnd(expr, i + 2);
|
|
450
|
+
if (interpolationEnd === -1)
|
|
451
|
+
return undefined;
|
|
452
|
+
interpolationParts.push(expr.slice(i + 2, interpolationEnd));
|
|
453
|
+
i = interpolationEnd + 1;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
text += c;
|
|
457
|
+
i += 1;
|
|
458
|
+
}
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
// Re-encode JS-template literal text (kept raw by parseTemplateLiteral, with `\x`
|
|
462
|
+
// as two characters) for a Python double-quoted string. Most JS escapes are
|
|
463
|
+
// ALSO valid Python escapes (`\n \t \r \b \f \v \\ \" \uXXXX \xXX \0`), so they
|
|
464
|
+
// are preserved verbatim — decoding then re-encoding them only risks corrupting
|
|
465
|
+
// the exotic ones (Codex reviews on 678e6bc1 and the escape-decoder commit).
|
|
466
|
+
// Only the JS-specific escapes that Python does not recognise are converted to
|
|
467
|
+
// the bare character: `\`` → backtick, `\$` → `$`, `\'` → `'`. A bare `"` (or a
|
|
468
|
+
// bare trailing backslash, or raw control char) is escaped so the literal stays
|
|
469
|
+
// valid.
|
|
470
|
+
function escapeJsTemplateTextForPy(raw) {
|
|
471
|
+
let out = '';
|
|
472
|
+
for (let i = 0; i < raw.length; i++) {
|
|
473
|
+
const c = raw[i];
|
|
474
|
+
if (c === '\\' && i + 1 < raw.length) {
|
|
475
|
+
const next = raw[i + 1];
|
|
476
|
+
if (next === '`' || next === '$' || next === "'") {
|
|
477
|
+
out += next; // JS-only escape → bare char (Python has no such escape)
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
out += `\\${next}`; // valid Python escape (\n, \uXXXX, \0, ...) — keep
|
|
481
|
+
}
|
|
482
|
+
i += 1;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (c === '\\')
|
|
486
|
+
out += '\\\\'; // lone trailing backslash
|
|
487
|
+
else if (c === '"')
|
|
488
|
+
out += '\\"';
|
|
489
|
+
else if (c === '\n')
|
|
490
|
+
out += '\\n';
|
|
491
|
+
else if (c === '\r')
|
|
492
|
+
out += '\\r';
|
|
493
|
+
else if (c === '\t')
|
|
494
|
+
out += '\\t';
|
|
495
|
+
else
|
|
496
|
+
out += c;
|
|
497
|
+
}
|
|
498
|
+
return out;
|
|
499
|
+
}
|
|
500
|
+
function escapePythonTemplateText(text, forFormatTemplate) {
|
|
501
|
+
const escaped = escapeJsTemplateTextForPy(text);
|
|
502
|
+
if (!forFormatTemplate)
|
|
503
|
+
return escaped;
|
|
504
|
+
// str.format treats { } as field markers, so literal braces must be doubled.
|
|
505
|
+
return escaped.replace(/{/g, '{{').replace(/}/g, '}}');
|
|
506
|
+
}
|
|
507
|
+
function lowerTemplateLiteralToPython(parsed, pathParams, bodyFields, authUser, imports) {
|
|
508
|
+
if (parsed.interpolationParts.length === 0) {
|
|
509
|
+
return `"${escapePythonTemplateText(parsed.textParts.join(''), false)}"`;
|
|
510
|
+
}
|
|
511
|
+
const rewrittenInterpolations = parsed.interpolationParts.map((part) => rewriteFastAPIExpr(part.trim(), pathParams, bodyFields, authUser, imports));
|
|
512
|
+
let fmt = '';
|
|
513
|
+
for (let i = 0; i < parsed.textParts.length; i++) {
|
|
514
|
+
fmt += escapePythonTemplateText(parsed.textParts[i], true);
|
|
515
|
+
if (i < parsed.interpolationParts.length)
|
|
516
|
+
fmt += '{}';
|
|
517
|
+
}
|
|
518
|
+
return `"${fmt}".format(${rewrittenInterpolations.join(', ')})`;
|
|
519
|
+
}
|
|
520
|
+
function extractTemplateLiterals(expr, pathParams, bodyFields, authUser, imports) {
|
|
521
|
+
let maskedExpr = '';
|
|
522
|
+
const replacements = [];
|
|
523
|
+
let quote = null;
|
|
524
|
+
for (let i = 0; i < expr.length;) {
|
|
525
|
+
const c = expr[i];
|
|
526
|
+
if (quote) {
|
|
527
|
+
maskedExpr += c;
|
|
528
|
+
if (c === '\\') {
|
|
529
|
+
maskedExpr += expr[i + 1] ?? '';
|
|
530
|
+
i += 2;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
if (c === quote)
|
|
534
|
+
quote = null;
|
|
535
|
+
i += 1;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (c === '"' || c === "'") {
|
|
539
|
+
quote = c;
|
|
540
|
+
maskedExpr += c;
|
|
541
|
+
i += 1;
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
if (c === '`') {
|
|
545
|
+
const parsed = parseTemplateLiteral(expr, i);
|
|
546
|
+
if (!parsed) {
|
|
547
|
+
maskedExpr += c;
|
|
548
|
+
i += 1;
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
const placeholder = `__KERN_TEMPLATE_${replacements.length}__`;
|
|
552
|
+
const lowered = lowerTemplateLiteralToPython(parsed, pathParams, bodyFields, authUser, imports);
|
|
553
|
+
replacements.push({ placeholder, lowered });
|
|
554
|
+
maskedExpr += placeholder;
|
|
555
|
+
i = parsed.endIndex + 1;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
maskedExpr += c;
|
|
559
|
+
i += 1;
|
|
560
|
+
}
|
|
561
|
+
return { maskedExpr, replacements };
|
|
562
|
+
}
|
|
563
|
+
// Lower JS spread elements to Python unpacking, choosing the operator from the
|
|
564
|
+
// enclosing bracket: `{...x}` → `{**x}`, `[...x]` / `f(...x)` → `[*x]` / `f(*x)`.
|
|
565
|
+
// Bracket-aware (a stack) and string-aware (skips quoted contents) so a literal
|
|
566
|
+
// "..." inside a string is left intact. Runs BEFORE the request-ref rewrites so
|
|
567
|
+
// that, e.g., `...user.roles` becomes `*user.roles` and the auth rewrite's
|
|
568
|
+
// `(?<!\.)` lookbehind no longer sees the spread's trailing dot.
|
|
569
|
+
function lowerSpreadElements(expr) {
|
|
570
|
+
let out = '';
|
|
571
|
+
const stack = [];
|
|
572
|
+
let i = 0;
|
|
573
|
+
while (i < expr.length) {
|
|
574
|
+
const ch = expr[i];
|
|
575
|
+
if (ch === '"' || ch === "'") {
|
|
576
|
+
const q = ch;
|
|
577
|
+
out += ch;
|
|
578
|
+
i++;
|
|
579
|
+
while (i < expr.length) {
|
|
580
|
+
out += expr[i];
|
|
581
|
+
if (expr[i] === '\\') {
|
|
582
|
+
i++;
|
|
583
|
+
if (i < expr.length)
|
|
584
|
+
out += expr[i];
|
|
585
|
+
i++;
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (expr[i] === q) {
|
|
589
|
+
i++;
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
i++;
|
|
593
|
+
}
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
if (ch === '{' || ch === '[' || ch === '(') {
|
|
597
|
+
stack.push(ch);
|
|
598
|
+
out += ch;
|
|
599
|
+
i++;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (ch === '}' || ch === ']' || ch === ')') {
|
|
603
|
+
stack.pop();
|
|
604
|
+
out += ch;
|
|
605
|
+
i++;
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if (ch === '.' && expr[i + 1] === '.' && expr[i + 2] === '.') {
|
|
609
|
+
out += stack[stack.length - 1] === '{' ? '**' : '*';
|
|
610
|
+
i += 3;
|
|
611
|
+
// Collapse whitespace after the operator so `{ ... body }` yields tight
|
|
612
|
+
// `{**body}` — the model_dump pass matches `**body`, not `** body` (Codex).
|
|
613
|
+
while (i < expr.length && /\s/.test(expr[i]))
|
|
614
|
+
i++;
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
out += ch;
|
|
618
|
+
i++;
|
|
619
|
+
}
|
|
620
|
+
return out;
|
|
621
|
+
}
|
|
622
|
+
export function rewriteFastAPIExpr(expr, pathParams, bodyFields = new Set(), authUser = false, imports) {
|
|
623
|
+
const { maskedExpr, replacements } = extractTemplateLiterals(expr, pathParams, bodyFields, authUser, imports);
|
|
624
|
+
let result = maskedExpr;
|
|
625
|
+
// Spread → unpacking first, so the request-ref rewrites below see clean
|
|
626
|
+
// operands (e.g. `*user.roles`, not `...user.roles`).
|
|
627
|
+
result = lowerSpreadElements(result);
|
|
628
|
+
// Expand object shorthand BEFORE Array.from lowering, so a shorthand length
|
|
629
|
+
// object `Array.from({ length }, …)` becomes `{ length: length }` and is
|
|
630
|
+
// recognised (codex review of d75a9d05). No later pass creates new object
|
|
631
|
+
// literals, so this single early pass covers length objects, arrow bodies,
|
|
632
|
+
// and every other object.
|
|
633
|
+
result = expandObjectShorthand(result);
|
|
634
|
+
// Array.from(length, arrow) → list comprehension. Runs before the ref/key
|
|
635
|
+
// passes so they lower the count and body of the produced comprehension.
|
|
636
|
+
result = lowerArrayFromCalls(result);
|
|
92
637
|
// params.X → X (function param) for path params
|
|
93
638
|
for (const param of pathParams) {
|
|
94
639
|
result = result.replace(new RegExp(`\\bparams\\.${param}\\b`, 'g'), param);
|
|
95
640
|
}
|
|
96
641
|
// Fallback: any remaining params.X → X (for query params not in pathParams)
|
|
97
642
|
result = result.replace(/\bparams\.([A-Za-z_]\w*)/g, '$1');
|
|
98
|
-
//
|
|
643
|
+
// user.X → user["X"]: with auth, `user` is the decoded JWT payload (a dict
|
|
644
|
+
// returned by auth_required/auth_optional), so attribute access would raise
|
|
645
|
+
// AttributeError. Only applied when the route declares auth (Codex review).
|
|
646
|
+
// Skip text inside string literals so `{{"user.id"}}` isn't corrupted to
|
|
647
|
+
// `"user["id"]"` (Codex review on 02ecb2fa), and require `user` NOT be a
|
|
648
|
+
// property of something else (negative lookbehind `(?<!\.)`) so a nested
|
|
649
|
+
// body access like `body.user.id` is left intact (Kimi review on 02ecb2fa).
|
|
650
|
+
if (authUser) {
|
|
651
|
+
const USER_FIELD_RE = new RegExp(`${STRING_LITERAL_ALT}|(?<!\\.)\\buser\\.([A-Za-z_]\\w*)`, 'g');
|
|
652
|
+
result = result.replace(USER_FIELD_RE, (match, field) => (field ? `user["${field}"]` : match));
|
|
653
|
+
}
|
|
654
|
+
// body.X → body.<snake_case(X)>: the generated Pydantic model snake-cases
|
|
655
|
+
// every field, so a camelCase access would raise AttributeError at runtime.
|
|
656
|
+
// Only remap fields the model actually declares; leave unknown `body.X`
|
|
657
|
+
// (e.g. external validate schemas) untouched.
|
|
658
|
+
result = result.replace(/\bbody\.([A-Za-z_]\w*)/g, (match, field) => bodyFields.has(field) ? `body.${toSnakeCase(field)}` : match);
|
|
659
|
+
// Spreading the whole request body: `{**body}` raises TypeError because a
|
|
660
|
+
// Pydantic model is not a mapping, so unpack its dict form instead. This is
|
|
661
|
+
// unconditional: whenever the `body` symbol exists it is a Pydantic model
|
|
662
|
+
// (inline `RequestBody`, or an external `validate` schema typed `body: X` for
|
|
663
|
+
// POST/PUT/PATCH) — there is no `body: dict` codegen path, so model_dump() is
|
|
664
|
+
// always correct. Keying on bodyFields would wrongly skip external schemas
|
|
665
|
+
// (their field names are unknown but the param is still a model). A
|
|
666
|
+
// `**body.field` member spread is left alone via the `(?!\s*\.)` guard.
|
|
667
|
+
result = result.replace(/\*\*body\b(?!\s*\.)/g, '**body.model_dump()');
|
|
99
668
|
// query.X → X (function param)
|
|
100
669
|
result = result.replace(/\bquery\.([A-Za-z_]\w*)/g, '$1');
|
|
101
670
|
// headers.X → request.headers.get("X")
|
|
@@ -127,6 +696,37 @@ export function rewriteFastAPIExpr(expr, pathParams) {
|
|
|
127
696
|
return 'False';
|
|
128
697
|
return match; // quoted string
|
|
129
698
|
});
|
|
699
|
+
// ── Host-builtin lowering (JS globals → Python stdlib) ────────────────
|
|
700
|
+
// crypto / Date are fixed forms matched by regex with a `(?<![\w.])` guard so
|
|
701
|
+
// a custom receiver (`some.crypto.randomUUID()`) is left untouched. The JSON
|
|
702
|
+
// calls need balanced argument parsing (regex can't), so they go through the
|
|
703
|
+
// string-aware scanner `lowerJsonBuiltinCalls`.
|
|
704
|
+
// crypto.randomUUID() → str(uuid.uuid4())
|
|
705
|
+
result = result.replace(new RegExp(`${STRING_LITERAL_ALT}|(?<![\\w.])crypto\\.randomUUID\\(\\)`, 'g'), (match) => {
|
|
706
|
+
if (match === 'crypto.randomUUID()') {
|
|
707
|
+
imports?.add('import uuid');
|
|
708
|
+
return 'str(uuid.uuid4())';
|
|
709
|
+
}
|
|
710
|
+
return match; // string literal — leave untouched
|
|
711
|
+
});
|
|
712
|
+
// new Date().toISOString() → datetime.now(timezone.utc).isoformat()
|
|
713
|
+
result = result.replace(new RegExp(`${STRING_LITERAL_ALT}|(?<![\\w.])new Date\\(\\)\\.toISOString\\(\\)`, 'g'), (match) => {
|
|
714
|
+
if (match === 'new Date().toISOString()') {
|
|
715
|
+
imports?.add('from datetime import datetime, timezone');
|
|
716
|
+
return 'datetime.now(timezone.utc).isoformat()';
|
|
717
|
+
}
|
|
718
|
+
return match;
|
|
719
|
+
});
|
|
720
|
+
// JSON.stringify(...) → json.dumps(...) / JSON.parse(...) → json.loads(...)
|
|
721
|
+
result = lowerJsonBuiltinCalls(result, imports);
|
|
722
|
+
// Object-literal keys → quoted Python dict keys (`{userId: x}` →
|
|
723
|
+
// `{"userId": x}`). Applied last, mirroring the raw `res.json(...)` path's
|
|
724
|
+
// outer quote-after-lower order; runs after array-method lowering so dicts
|
|
725
|
+
// produced inside list comprehensions are quoted too.
|
|
726
|
+
result = quoteObjectKeysOutsideStrings(result);
|
|
727
|
+
for (const replacement of replacements) {
|
|
728
|
+
result = result.split(replacement.placeholder).join(replacement.lowered);
|
|
729
|
+
}
|
|
130
730
|
return result;
|
|
131
731
|
}
|
|
132
732
|
export function extractExprCode(prop) {
|