tova 0.2.9 → 0.3.1
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/bin/tova.js +1404 -114
- package/package.json +3 -1
- package/src/analyzer/analyzer.js +882 -695
- package/src/analyzer/client-analyzer.js +191 -0
- package/src/analyzer/server-analyzer.js +467 -0
- package/src/analyzer/types.js +20 -4
- package/src/codegen/base-codegen.js +473 -111
- package/src/codegen/client-codegen.js +109 -46
- package/src/codegen/codegen.js +65 -5
- package/src/codegen/server-codegen.js +297 -38
- package/src/diagnostics/error-codes.js +255 -0
- package/src/diagnostics/formatter.js +150 -28
- package/src/docs/generator.js +390 -0
- package/src/lexer/lexer.js +306 -64
- package/src/lexer/tokens.js +19 -0
- package/src/lsp/server.js +935 -53
- package/src/parser/ast.js +81 -368
- package/src/parser/client-ast.js +138 -0
- package/src/parser/client-parser.js +504 -0
- package/src/parser/parser.js +492 -1056
- package/src/parser/server-ast.js +240 -0
- package/src/parser/server-parser.js +602 -0
- package/src/runtime/array-proto.js +32 -0
- package/src/runtime/embedded.js +1 -1
- package/src/runtime/reactivity.js +239 -42
- package/src/stdlib/advanced-collections.js +81 -0
- package/src/stdlib/inline.js +556 -13
- package/src/version.js +1 -1
|
@@ -156,7 +156,7 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
156
156
|
const modelDecls = [];
|
|
157
157
|
const aiConfigs = []; // { name: string|null, config: object }
|
|
158
158
|
|
|
159
|
-
const collectFromBody = (stmts, groupPrefix = null, groupMiddlewares = []) => {
|
|
159
|
+
const collectFromBody = (stmts, groupPrefix = null, groupMiddlewares = [], groupVersion = null) => {
|
|
160
160
|
for (const stmt of stmts) {
|
|
161
161
|
if (stmt.type === 'RouteDeclaration') {
|
|
162
162
|
const route = stmt;
|
|
@@ -165,6 +165,7 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
165
165
|
...route,
|
|
166
166
|
path: groupPrefix + route.path,
|
|
167
167
|
_groupMiddlewares: groupMiddlewares.length > 0 ? [...groupMiddlewares] : undefined,
|
|
168
|
+
_version: groupVersion || undefined,
|
|
168
169
|
};
|
|
169
170
|
routes.push(prefixedRoute);
|
|
170
171
|
} else {
|
|
@@ -196,7 +197,8 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
196
197
|
} else if (stmt.type === 'RouteGroupDeclaration') {
|
|
197
198
|
const prefix = groupPrefix ? groupPrefix + stmt.prefix : stmt.prefix;
|
|
198
199
|
const grpMw = [...groupMiddlewares]; // inherit parent group middlewares
|
|
199
|
-
|
|
200
|
+
const ver = stmt.version || groupVersion; // inherit or override version
|
|
201
|
+
collectFromBody(stmt.body, prefix, grpMw, ver);
|
|
200
202
|
} else if (stmt.type === 'RateLimitDeclaration') {
|
|
201
203
|
rateLimitConfig = stmt.config;
|
|
202
204
|
} else if (stmt.type === 'LifecycleHookDeclaration') {
|
|
@@ -240,8 +242,8 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
240
242
|
|
|
241
243
|
// Collect type declarations from shared blocks for model/ORM generation
|
|
242
244
|
const sharedTypes = new Map(); // typeName -> { fields: [{ name, type }] }
|
|
243
|
-
|
|
244
|
-
for (const stmt of
|
|
245
|
+
const _collectTypes = (stmts) => {
|
|
246
|
+
for (const stmt of stmts) {
|
|
245
247
|
if (stmt.type === 'TypeDeclaration' && stmt.variants) {
|
|
246
248
|
const fields = [];
|
|
247
249
|
for (const v of stmt.variants) {
|
|
@@ -254,6 +256,13 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
254
256
|
}
|
|
255
257
|
}
|
|
256
258
|
}
|
|
259
|
+
};
|
|
260
|
+
for (const sb of sharedBlocks) {
|
|
261
|
+
_collectTypes(sb.body);
|
|
262
|
+
}
|
|
263
|
+
// Also collect types from server blocks (for body: Type validation)
|
|
264
|
+
for (const block of serverBlocks) {
|
|
265
|
+
_collectTypes(block.body);
|
|
257
266
|
}
|
|
258
267
|
|
|
259
268
|
// Separate group-only middlewares from global middlewares
|
|
@@ -474,6 +483,7 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
474
483
|
lines.push(this._getAiRuntime());
|
|
475
484
|
lines.push('');
|
|
476
485
|
|
|
486
|
+
let hasDefaultAi = false;
|
|
477
487
|
for (const aiConf of aiConfigs) {
|
|
478
488
|
const configParts = [];
|
|
479
489
|
for (const [key, valueNode] of Object.entries(aiConf.config)) {
|
|
@@ -487,8 +497,13 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
487
497
|
} else {
|
|
488
498
|
// Default provider: ai { ... } → const ai = __createAI({...})
|
|
489
499
|
lines.push(`const ai = __createAI(${configStr});`);
|
|
500
|
+
hasDefaultAi = true;
|
|
490
501
|
}
|
|
491
502
|
}
|
|
503
|
+
// If no default ai config, create a default for one-off calls
|
|
504
|
+
if (!hasDefaultAi) {
|
|
505
|
+
lines.push('const ai = __createAI({});');
|
|
506
|
+
}
|
|
492
507
|
lines.push('');
|
|
493
508
|
}
|
|
494
509
|
|
|
@@ -782,12 +797,12 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
782
797
|
// ════════════════════════════════════════════════════════════
|
|
783
798
|
lines.push('// ── Router ──');
|
|
784
799
|
lines.push('const __routes = [];');
|
|
785
|
-
lines.push('function __addRoute(method, path, handler) {');
|
|
800
|
+
lines.push('function __addRoute(method, path, handler, version) {');
|
|
786
801
|
lines.push(' let pattern = path');
|
|
787
802
|
lines.push(' .replace(/\\*([a-zA-Z_][a-zA-Z0-9_]*)/g, "(?<$1>.+)")');
|
|
788
803
|
lines.push(' .replace(/\\*$/g, "(.*)")');
|
|
789
804
|
lines.push(' .replace(/:([^/]+)/g, "(?<$1>[^/]+)");');
|
|
790
|
-
lines.push(' __routes.push({ method, regex: new RegExp(`^${pattern}$`), handler, _path: path });');
|
|
805
|
+
lines.push(' __routes.push({ method, regex: new RegExp(`^${pattern}$`), handler, _path: path, _version: version || null });');
|
|
791
806
|
lines.push('}');
|
|
792
807
|
lines.push('');
|
|
793
808
|
|
|
@@ -1693,6 +1708,10 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
1693
1708
|
const validateDec = decorators.find(d => d.name === 'validate');
|
|
1694
1709
|
const uploadDec = decorators.find(d => d.name === 'upload');
|
|
1695
1710
|
|
|
1711
|
+
// T9-5: Version info for route
|
|
1712
|
+
const routeVersion = route._version;
|
|
1713
|
+
const versionArg = routeVersion ? `, ${JSON.stringify(String(routeVersion.version || ''))}` : '';
|
|
1714
|
+
|
|
1696
1715
|
lines.push(`__addRoute(${JSON.stringify(method)}, ${JSON.stringify(path)}, async (req, params) => {`);
|
|
1697
1716
|
|
|
1698
1717
|
// Auth decorator check
|
|
@@ -1735,6 +1754,68 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
1735
1754
|
lines.push(` if (__validationErrors.length > 0) return Response.json({ error: "Validation failed", details: __validationErrors }, { status: 400 });`);
|
|
1736
1755
|
}
|
|
1737
1756
|
|
|
1757
|
+
// T9-1: Route-level body type validation — route POST "/api/users" body: User => handler
|
|
1758
|
+
if (route.bodyType && ['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
1759
|
+
const typeName = route.bodyType.name || (route.bodyType.elementType && route.bodyType.elementType.name);
|
|
1760
|
+
if (typeName && sharedTypes.has(typeName)) {
|
|
1761
|
+
const typeInfo = sharedTypes.get(typeName);
|
|
1762
|
+
if (!uploadDec && !validateDec) {
|
|
1763
|
+
lines.push(` const __body = (await __parseBody(req)) || {};`);
|
|
1764
|
+
}
|
|
1765
|
+
const isArray = route.bodyType.type === 'ArrayTypeAnnotation';
|
|
1766
|
+
if (isArray) {
|
|
1767
|
+
lines.push(` if (!Array.isArray(__body)) return Response.json({ error: "Request body must be an array of ${typeName}" }, { status: 400 });`);
|
|
1768
|
+
lines.push(` const __bodyTypeErrors = [];`);
|
|
1769
|
+
lines.push(` for (let __i = 0; __i < __body.length; __i++) {`);
|
|
1770
|
+
lines.push(` const __item = __body[__i];`);
|
|
1771
|
+
for (const f of typeInfo.fields) {
|
|
1772
|
+
if (f.name === 'id') continue;
|
|
1773
|
+
switch (f.type) {
|
|
1774
|
+
case 'String':
|
|
1775
|
+
lines.push(` if (__item.${f.name} !== undefined && typeof __item.${f.name} !== "string") __bodyTypeErrors.push(\`[${f.name}] at index \${__i} must be a string\`);`);
|
|
1776
|
+
break;
|
|
1777
|
+
case 'Int':
|
|
1778
|
+
lines.push(` if (__item.${f.name} !== undefined && !Number.isInteger(__item.${f.name})) __bodyTypeErrors.push(\`[${f.name}] at index \${__i} must be an integer\`);`);
|
|
1779
|
+
break;
|
|
1780
|
+
case 'Float':
|
|
1781
|
+
lines.push(` if (__item.${f.name} !== undefined && typeof __item.${f.name} !== "number") __bodyTypeErrors.push(\`[${f.name}] at index \${__i} must be a number\`);`);
|
|
1782
|
+
break;
|
|
1783
|
+
case 'Bool':
|
|
1784
|
+
lines.push(` if (__item.${f.name} !== undefined && typeof __item.${f.name} !== "boolean") __bodyTypeErrors.push(\`[${f.name}] at index \${__i} must be a boolean\`);`);
|
|
1785
|
+
break;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
lines.push(` }`);
|
|
1789
|
+
lines.push(` if (__bodyTypeErrors.length > 0) return Response.json({ error: "Validation failed for ${typeName}[]", details: __bodyTypeErrors }, { status: 400 });`);
|
|
1790
|
+
} else {
|
|
1791
|
+
lines.push(` const __bodyTypeErrors = [];`);
|
|
1792
|
+
for (const f of typeInfo.fields) {
|
|
1793
|
+
if (f.name === 'id') continue;
|
|
1794
|
+
switch (f.type) {
|
|
1795
|
+
case 'String':
|
|
1796
|
+
lines.push(` if (__body.${f.name} !== undefined && typeof __body.${f.name} !== "string") __bodyTypeErrors.push("${f.name} must be a string");`);
|
|
1797
|
+
break;
|
|
1798
|
+
case 'Int':
|
|
1799
|
+
lines.push(` if (__body.${f.name} !== undefined && !Number.isInteger(__body.${f.name})) __bodyTypeErrors.push("${f.name} must be an integer");`);
|
|
1800
|
+
break;
|
|
1801
|
+
case 'Float':
|
|
1802
|
+
lines.push(` if (__body.${f.name} !== undefined && typeof __body.${f.name} !== "number") __bodyTypeErrors.push("${f.name} must be a number");`);
|
|
1803
|
+
break;
|
|
1804
|
+
case 'Bool':
|
|
1805
|
+
lines.push(` if (__body.${f.name} !== undefined && typeof __body.${f.name} !== "boolean") __bodyTypeErrors.push("${f.name} must be a boolean");`);
|
|
1806
|
+
break;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
// Check for required fields (non-id fields without defaults)
|
|
1810
|
+
for (const f of typeInfo.fields) {
|
|
1811
|
+
if (f.name === 'id') continue;
|
|
1812
|
+
lines.push(` if (__body.${f.name} === undefined || __body.${f.name} === null) __bodyTypeErrors.push("${f.name} is required");`);
|
|
1813
|
+
}
|
|
1814
|
+
lines.push(` if (__bodyTypeErrors.length > 0) return Response.json({ error: "Validation failed for type ${typeName}", details: __bodyTypeErrors }, { status: 400 });`);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1738
1819
|
// Type-safe body deserialization: if a param has a shared type annotation, auto-validate
|
|
1739
1820
|
if (handlerDecl && ['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
1740
1821
|
for (const p of handlerDecl.params) {
|
|
@@ -1851,9 +1932,65 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
1851
1932
|
this._emitHandlerCall(lines, `${handler}(req, params)`, timeoutMs);
|
|
1852
1933
|
}
|
|
1853
1934
|
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1935
|
+
// T9-3: Detect generator-based streaming — if handler uses yield, wrap in streaming response
|
|
1936
|
+
const isGeneratorHandler = route.handler.type === 'Identifier'
|
|
1937
|
+
? (handlerDecl && this._containsYield(handlerDecl.body))
|
|
1938
|
+
: (route.handler.type === 'FunctionDeclaration' || route.handler.type === 'LambdaExpression')
|
|
1939
|
+
? this._containsYield(route.handler.body || route.handler)
|
|
1940
|
+
: false;
|
|
1941
|
+
|
|
1942
|
+
if (isGeneratorHandler) {
|
|
1943
|
+
lines.push(` if (__result && typeof __result[Symbol.asyncIterator] === "function") {`);
|
|
1944
|
+
lines.push(` const __encoder = new TextEncoder();`);
|
|
1945
|
+
lines.push(` const __stream = new ReadableStream({`);
|
|
1946
|
+
lines.push(` async start(controller) {`);
|
|
1947
|
+
lines.push(` try {`);
|
|
1948
|
+
lines.push(` for await (const __chunk of __result) {`);
|
|
1949
|
+
lines.push(` const __data = typeof __chunk === "string" ? __chunk : JSON.stringify(__chunk);`);
|
|
1950
|
+
lines.push(` controller.enqueue(__encoder.encode(\`data: \${__data}\\n\\n\`));`);
|
|
1951
|
+
lines.push(` }`);
|
|
1952
|
+
lines.push(` } finally { controller.close(); }`);
|
|
1953
|
+
lines.push(` }`);
|
|
1954
|
+
lines.push(` });`);
|
|
1955
|
+
lines.push(` return new Response(__stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" } });`);
|
|
1956
|
+
lines.push(` }`);
|
|
1957
|
+
lines.push(` if (__result && typeof __result[Symbol.iterator] === "function" && typeof __result !== "string") {`);
|
|
1958
|
+
lines.push(` const __encoder = new TextEncoder();`);
|
|
1959
|
+
lines.push(` const __chunks = [...__result];`);
|
|
1960
|
+
lines.push(` const __stream = new ReadableStream({`);
|
|
1961
|
+
lines.push(` start(controller) {`);
|
|
1962
|
+
lines.push(` for (const __chunk of __chunks) {`);
|
|
1963
|
+
lines.push(` const __data = typeof __chunk === "string" ? __chunk : JSON.stringify(__chunk);`);
|
|
1964
|
+
lines.push(` controller.enqueue(__encoder.encode(\`data: \${__data}\\n\\n\`));`);
|
|
1965
|
+
lines.push(` }`);
|
|
1966
|
+
lines.push(` controller.close();`);
|
|
1967
|
+
lines.push(` }`);
|
|
1968
|
+
lines.push(` });`);
|
|
1969
|
+
lines.push(` return new Response(__stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" } });`);
|
|
1970
|
+
lines.push(` }`);
|
|
1971
|
+
}
|
|
1972
|
+
// T9-5: API versioning — add version headers to responses
|
|
1973
|
+
if (routeVersion) {
|
|
1974
|
+
const ver = JSON.stringify(String(routeVersion.version || ''));
|
|
1975
|
+
lines.push(` const __addVersionHeaders = (res) => {`);
|
|
1976
|
+
lines.push(` const h = new Headers(res.headers);`);
|
|
1977
|
+
lines.push(` h.set("API-Version", ${ver});`);
|
|
1978
|
+
if (routeVersion.deprecated) {
|
|
1979
|
+
lines.push(` h.set("Deprecation", "true");`);
|
|
1980
|
+
if (routeVersion.sunset) {
|
|
1981
|
+
lines.push(` h.set("Sunset", ${JSON.stringify(String(routeVersion.sunset))});`);
|
|
1982
|
+
}
|
|
1983
|
+
lines.push(` h.set("Link", '</api/v' + (parseInt(${ver}) + 1) + req.url.replace(/\\/api\\/v\\d+/, "") + '>; rel="successor-version"');`);
|
|
1984
|
+
}
|
|
1985
|
+
lines.push(` return new Response(res.body, { status: res.status, headers: h });`);
|
|
1986
|
+
lines.push(` };`);
|
|
1987
|
+
lines.push(` if (__result instanceof Response) return __addVersionHeaders(__result);`);
|
|
1988
|
+
lines.push(` return __addVersionHeaders(Response.json(__result));`);
|
|
1989
|
+
} else {
|
|
1990
|
+
lines.push(` if (__result instanceof Response) return __result;`);
|
|
1991
|
+
lines.push(` return Response.json(__result);`);
|
|
1992
|
+
}
|
|
1993
|
+
lines.push(`}${versionArg});`);
|
|
1857
1994
|
lines.push('');
|
|
1858
1995
|
}
|
|
1859
1996
|
}
|
|
@@ -1922,46 +2059,66 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
1922
2059
|
lines.push(' ],');
|
|
1923
2060
|
}
|
|
1924
2061
|
|
|
1925
|
-
// Request body schema for POST/PUT/PATCH
|
|
1926
|
-
if (['post', 'put', 'patch'].includes(method)
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
lines.push(` ${bp.name}: { "$ref": "#/components/schemas/${ta.name}" },`);
|
|
1935
|
-
} else if (ta) {
|
|
1936
|
-
let jsonType;
|
|
1937
|
-
switch (ta.name) {
|
|
1938
|
-
case 'Int': jsonType = '"integer"'; break;
|
|
1939
|
-
case 'Float': jsonType = '"number"'; break;
|
|
1940
|
-
case 'Bool': jsonType = '"boolean"'; break;
|
|
1941
|
-
default: jsonType = '"string"'; break;
|
|
1942
|
-
}
|
|
1943
|
-
lines.push(` ${bp.name}: { type: ${jsonType} },`);
|
|
2062
|
+
// Request body schema for POST/PUT/PATCH — prefer route-level bodyType, fall back to handler params
|
|
2063
|
+
if (['post', 'put', 'patch'].includes(method)) {
|
|
2064
|
+
if (route.bodyType) {
|
|
2065
|
+
// T9-1: Route-level body type annotation
|
|
2066
|
+
const bt = route.bodyType;
|
|
2067
|
+
if (bt.type === 'ArrayTypeAnnotation' && bt.elementType) {
|
|
2068
|
+
const elName = bt.elementType.name;
|
|
2069
|
+
if (sharedTypes.has(elName)) {
|
|
2070
|
+
lines.push(` requestBody: { required: true, content: { "application/json": { schema: { type: "array", items: { "$ref": "#/components/schemas/${elName}" } } } } },`);
|
|
1944
2071
|
} else {
|
|
1945
|
-
lines.push(`
|
|
2072
|
+
lines.push(` requestBody: { required: true, content: { "application/json": { schema: { type: "array", items: ${tovaTypeToJsonSchema(elName)} } } } },`);
|
|
1946
2073
|
}
|
|
2074
|
+
} else if (bt.type === 'TypeAnnotation' && sharedTypes.has(bt.name)) {
|
|
2075
|
+
lines.push(` requestBody: { required: true, content: { "application/json": { schema: { "$ref": "#/components/schemas/${bt.name}" } } } },`);
|
|
2076
|
+
} else if (bt.type === 'TypeAnnotation') {
|
|
2077
|
+
lines.push(` requestBody: { required: true, content: { "application/json": { schema: ${tovaTypeToJsonSchema(bt.name)} } } },`);
|
|
2078
|
+
}
|
|
2079
|
+
} else if (handlerDecl) {
|
|
2080
|
+
const bodyParams = handlerDecl.params.filter(p => p.name !== 'req' && !pathParams.includes(p.name));
|
|
2081
|
+
if (bodyParams.length > 0) {
|
|
2082
|
+
lines.push(' requestBody: {');
|
|
2083
|
+
lines.push(' content: { "application/json": { schema: { type: "object", properties: {');
|
|
2084
|
+
for (const bp of bodyParams) {
|
|
2085
|
+
const ta = bp.typeAnnotation;
|
|
2086
|
+
if (ta && ta.name && sharedTypes.has(ta.name)) {
|
|
2087
|
+
lines.push(` ${bp.name}: { "$ref": "#/components/schemas/${ta.name}" },`);
|
|
2088
|
+
} else if (ta) {
|
|
2089
|
+
let jsonType;
|
|
2090
|
+
switch (ta.name) {
|
|
2091
|
+
case 'Int': jsonType = '"integer"'; break;
|
|
2092
|
+
case 'Float': jsonType = '"number"'; break;
|
|
2093
|
+
case 'Bool': jsonType = '"boolean"'; break;
|
|
2094
|
+
default: jsonType = '"string"'; break;
|
|
2095
|
+
}
|
|
2096
|
+
lines.push(` ${bp.name}: { type: ${jsonType} },`);
|
|
2097
|
+
} else {
|
|
2098
|
+
lines.push(` ${bp.name}: { type: "string" },`);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
lines.push(' } } } },');
|
|
2102
|
+
lines.push(' },');
|
|
1947
2103
|
}
|
|
1948
|
-
lines.push(' } } } },');
|
|
1949
|
-
lines.push(' },');
|
|
1950
2104
|
}
|
|
1951
2105
|
}
|
|
1952
2106
|
|
|
1953
|
-
// Response schema
|
|
1954
|
-
|
|
1955
|
-
|
|
2107
|
+
// Response schema — prefer route-level responseType (T9-2), fall back to handler return type
|
|
2108
|
+
const responseType = route.responseType || (handlerDecl && handlerDecl.returnType);
|
|
2109
|
+
if (responseType) {
|
|
2110
|
+
const rt = responseType;
|
|
1956
2111
|
if (rt.type === 'ArrayTypeAnnotation' && rt.elementType) {
|
|
1957
2112
|
const elName = rt.elementType.name;
|
|
1958
2113
|
if (sharedTypes.has(elName)) {
|
|
1959
2114
|
lines.push(` responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "array", items: { "$ref": "#/components/schemas/${elName}" } } } } } },`);
|
|
1960
2115
|
} else {
|
|
1961
|
-
lines.push(
|
|
2116
|
+
lines.push(` responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "array", items: ${tovaTypeToJsonSchema(elName)} } } } } },`);
|
|
1962
2117
|
}
|
|
1963
2118
|
} else if (rt.type === 'TypeAnnotation' && sharedTypes.has(rt.name)) {
|
|
1964
2119
|
lines.push(` responses: { "200": { description: "Success", content: { "application/json": { schema: { "$ref": "#/components/schemas/${rt.name}" } } } } },`);
|
|
2120
|
+
} else if (rt.type === 'TypeAnnotation') {
|
|
2121
|
+
lines.push(` responses: { "200": { description: "Success", content: { "application/json": { schema: ${tovaTypeToJsonSchema(rt.name)} } } } },`);
|
|
1965
2122
|
} else {
|
|
1966
2123
|
lines.push(' responses: { "200": { description: "Success" } },');
|
|
1967
2124
|
}
|
|
@@ -1988,6 +2145,32 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
1988
2145
|
lines.push('');
|
|
1989
2146
|
}
|
|
1990
2147
|
|
|
2148
|
+
// T9-5: API versions endpoint — list available versions
|
|
2149
|
+
const versionedRoutes = routes.filter(r => r._version);
|
|
2150
|
+
if (versionedRoutes.length > 0) {
|
|
2151
|
+
const versionMap = new Map();
|
|
2152
|
+
for (const r of versionedRoutes) {
|
|
2153
|
+
const v = String(r._version.version || '');
|
|
2154
|
+
if (!versionMap.has(v)) {
|
|
2155
|
+
versionMap.set(v, { version: v, deprecated: !!r._version.deprecated, sunset: r._version.sunset || null });
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
lines.push('// ── API Versions ──');
|
|
2159
|
+
lines.push('__addRoute("GET", "/api/versions", async () => {');
|
|
2160
|
+
lines.push(' return Response.json({');
|
|
2161
|
+
lines.push(' versions: [');
|
|
2162
|
+
for (const [, info] of versionMap) {
|
|
2163
|
+
const parts = [`version: ${JSON.stringify(info.version)}`];
|
|
2164
|
+
if (info.deprecated) parts.push('deprecated: true');
|
|
2165
|
+
if (info.sunset) parts.push(`sunset: ${JSON.stringify(info.sunset)}`);
|
|
2166
|
+
lines.push(` { ${parts.join(', ')} },`);
|
|
2167
|
+
}
|
|
2168
|
+
lines.push(' ]');
|
|
2169
|
+
lines.push(' });');
|
|
2170
|
+
lines.push('});');
|
|
2171
|
+
lines.push('');
|
|
2172
|
+
}
|
|
2173
|
+
|
|
1991
2174
|
// Include __contains helper if needed
|
|
1992
2175
|
if (this._needsContainsHelper) {
|
|
1993
2176
|
lines.push(this.getContainsHelper());
|
|
@@ -2521,7 +2704,31 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
2521
2704
|
|
|
2522
2705
|
for (const block of testBlocks) {
|
|
2523
2706
|
const name = block.name || 'Tests';
|
|
2707
|
+
const blockTimeout = block.timeout || null;
|
|
2524
2708
|
lines.push(`describe(${JSON.stringify(name)}, () => {`);
|
|
2709
|
+
|
|
2710
|
+
// Emit beforeEach if defined
|
|
2711
|
+
if (block.beforeEach && block.beforeEach.length > 0) {
|
|
2712
|
+
lines.push(' beforeEach(async () => {');
|
|
2713
|
+
this.pushScope();
|
|
2714
|
+
for (const s of block.beforeEach) {
|
|
2715
|
+
lines.push(' ' + this.generateStatement(s));
|
|
2716
|
+
}
|
|
2717
|
+
this.popScope();
|
|
2718
|
+
lines.push(' });');
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
// Emit afterEach if defined
|
|
2722
|
+
if (block.afterEach && block.afterEach.length > 0) {
|
|
2723
|
+
lines.push(' afterEach(async () => {');
|
|
2724
|
+
this.pushScope();
|
|
2725
|
+
for (const s of block.afterEach) {
|
|
2726
|
+
lines.push(' ' + this.generateStatement(s));
|
|
2727
|
+
}
|
|
2728
|
+
this.popScope();
|
|
2729
|
+
lines.push(' });');
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2525
2732
|
for (const stmt of block.body) {
|
|
2526
2733
|
if (stmt.type === 'FunctionDeclaration') {
|
|
2527
2734
|
const fnName = stmt.name;
|
|
@@ -2533,9 +2740,10 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
2533
2740
|
}
|
|
2534
2741
|
const body = this.genBlockBody(stmt.body);
|
|
2535
2742
|
this.popScope();
|
|
2743
|
+
const timeoutArg = blockTimeout ? `, ${blockTimeout}` : '';
|
|
2536
2744
|
lines.push(` test(${JSON.stringify(displayName)}, async () => {`);
|
|
2537
2745
|
lines.push(body);
|
|
2538
|
-
lines.push(
|
|
2746
|
+
lines.push(` }${timeoutArg});`);
|
|
2539
2747
|
} else {
|
|
2540
2748
|
lines.push(' ' + this.generateStatement(stmt));
|
|
2541
2749
|
}
|
|
@@ -2547,6 +2755,58 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
2547
2755
|
return lines.join('\n');
|
|
2548
2756
|
}
|
|
2549
2757
|
|
|
2758
|
+
generateBench(benchBlocks) {
|
|
2759
|
+
const lines = [];
|
|
2760
|
+
lines.push('// ── Tova Benchmark Runner ──');
|
|
2761
|
+
lines.push('');
|
|
2762
|
+
lines.push('async function __runBench(name, fn, runs) {');
|
|
2763
|
+
lines.push(' runs = runs || 100;');
|
|
2764
|
+
lines.push(' // Warmup');
|
|
2765
|
+
lines.push(' for (let i = 0; i < Math.min(10, runs); i++) await fn();');
|
|
2766
|
+
lines.push(' const times = [];');
|
|
2767
|
+
lines.push(' for (let i = 0; i < runs; i++) {');
|
|
2768
|
+
lines.push(' const start = performance.now();');
|
|
2769
|
+
lines.push(' await fn();');
|
|
2770
|
+
lines.push(' times.push(performance.now() - start);');
|
|
2771
|
+
lines.push(' }');
|
|
2772
|
+
lines.push(' times.sort((a, b) => a - b);');
|
|
2773
|
+
lines.push(' const sum = times.reduce((a, b) => a + b, 0);');
|
|
2774
|
+
lines.push(' const mean = sum / times.length;');
|
|
2775
|
+
lines.push(' const p50 = times[Math.floor(times.length * 0.5)];');
|
|
2776
|
+
lines.push(' const p99 = times[Math.floor(times.length * 0.99)];');
|
|
2777
|
+
lines.push(' console.log(`bench ${JSON.stringify(name)}: mean=${mean.toFixed(2)}ms p50=${p50.toFixed(2)}ms p99=${p99.toFixed(2)}ms (${runs} runs)`);');
|
|
2778
|
+
lines.push('}');
|
|
2779
|
+
lines.push('');
|
|
2780
|
+
lines.push('(async () => {');
|
|
2781
|
+
|
|
2782
|
+
for (const block of benchBlocks) {
|
|
2783
|
+
const name = block.name || 'Benchmark';
|
|
2784
|
+
lines.push(` console.log("── ${name.replace(/"/g, '\\"')} ──");`);
|
|
2785
|
+
for (const stmt of block.body) {
|
|
2786
|
+
if (stmt.type === 'FunctionDeclaration') {
|
|
2787
|
+
const fnName = stmt.name;
|
|
2788
|
+
const displayName = fnName.replace(/_/g, ' ');
|
|
2789
|
+
this.pushScope();
|
|
2790
|
+
for (const p of (stmt.params || [])) {
|
|
2791
|
+
const pName = typeof p === 'string' ? p : (p.name || p.identifier);
|
|
2792
|
+
if (pName) this.declareVar(pName);
|
|
2793
|
+
}
|
|
2794
|
+
const body = this.genBlockBody(stmt.body);
|
|
2795
|
+
this.popScope();
|
|
2796
|
+
lines.push(` await __runBench(${JSON.stringify(displayName)}, async () => {`);
|
|
2797
|
+
lines.push(body);
|
|
2798
|
+
lines.push(' });');
|
|
2799
|
+
} else {
|
|
2800
|
+
lines.push(' ' + this.generateStatement(stmt));
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
lines.push('');
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
lines.push('})();');
|
|
2807
|
+
return lines.join('\n');
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2550
2810
|
_getAiRuntime() {
|
|
2551
2811
|
return `// AI Client Runtime
|
|
2552
2812
|
function __createAI(config) {
|
|
@@ -2672,7 +2932,6 @@ function __createAI(config) {
|
|
|
2672
2932
|
classify(text, categories, opts) { return __aiRequest('classify', [text, categories, opts || {}], opts); },
|
|
2673
2933
|
};
|
|
2674
2934
|
}
|
|
2675
|
-
|
|
2676
|
-
const ai = typeof ai === 'undefined' ? __createAI({}) : ai;`;
|
|
2935
|
+
`;
|
|
2677
2936
|
}
|
|
2678
2937
|
}
|