@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
358 lines • 15.1 kB
JavaScript
/**
* @fileoverview OrdoJS RPC Generator - Automatic RPC stub generation
*/
import {} from '../types/index.js';
/**
* Default RPC generator options
*/
const DEFAULT_RPC_OPTIONS = {
endpoint: '/api/rpc',
timeout: 30000,
retries: 3,
errorHandling: 'throw',
authentication: false,
compression: false
};
/**
* RPC Generator for automatic stub generation
*/
export class RPCGenerator {
options;
indentLevel = 0;
constructor(options = {}) {
this.options = { ...DEFAULT_RPC_OPTIONS, ...options };
}
/**
* Generate RPC stubs for all public server functions in a component
*/
generateRPCStubs(ast) {
if (!ast.component.serverBlock) {
return [];
}
const publicFunctions = ast.component.serverBlock.functions.filter(f => f.isPublic);
const stubs = [];
for (const func of publicFunctions) {
const metadata = {
componentName: ast.component.name,
functionName: func.name,
parameters: func.parameters,
returnType: func.returnType,
isAsync: func.isAsync,
middleware: func.middleware,
permissions: func.permissions
};
const stub = {
functionName: func.name,
clientCode: this.generateClientStub(metadata),
serverEndpoint: this.generateServerEndpoint(metadata),
metadata
};
stubs.push(stub);
}
return stubs;
}
/**
* Generate client-side RPC stub code
*/
generateClientStub(metadata) {
this.indentLevel = 0;
const lines = [];
// Generate function signature
const paramString = this.generateParameterString(metadata.parameters);
const asyncKeyword = metadata.isAsync ? 'async ' : '';
lines.push(`${asyncKeyword}function ${metadata.functionName}(${paramString}) {`);
this.indentLevel++;
// Add parameter validation if needed
if (metadata.parameters.length > 0) {
lines.push(this.indent('// Parameter validation'));
for (const param of metadata.parameters) {
if (!param.isOptional) {
lines.push(this.indent(`if (${param.name} === undefined || ${param.name} === null) {`));
this.indentLevel++;
lines.push(this.indent(`throw new Error('Required parameter "${param.name}" is missing');`));
this.indentLevel--;
lines.push(this.indent('}'));
}
}
lines.push('');
}
// Generate request payload
lines.push(this.indent('// Prepare request payload'));
const payloadParams = metadata.parameters.map(p => p.name).join(', ');
lines.push(this.indent(`const payload = { ${payloadParams} };`));
lines.push('');
// Generate fetch configuration
lines.push(this.indent('// Configure request'));
lines.push(this.indent('const requestConfig = {'));
this.indentLevel++;
lines.push(this.indent('method: "POST",'));
lines.push(this.indent('headers: {'));
this.indentLevel++;
lines.push(this.indent('"Content-Type": "application/json",'));
if (this.options.authentication) {
lines.push(this.indent('"Authorization": `Bearer ${getAuthToken()}`,'));
}
if (this.options.compression) {
lines.push(this.indent('"Accept-Encoding": "gzip, deflate, br",'));
}
this.indentLevel--;
lines.push(this.indent('},'));
lines.push(this.indent('body: JSON.stringify(payload)'));
this.indentLevel--;
lines.push(this.indent('};'));
lines.push('');
// Add timeout if specified
if (this.options.timeout && this.options.timeout > 0) {
lines.push(this.indent('// Add timeout'));
lines.push(this.indent('const controller = new AbortController();'));
lines.push(this.indent(`const timeoutId = setTimeout(() => controller.abort(), ${this.options.timeout});`));
lines.push(this.indent('requestConfig.signal = controller.signal;'));
lines.push('');
}
// Generate retry logic
if (this.options.retries && this.options.retries > 0) {
lines.push(this.indent('// Retry logic'));
lines.push(this.indent(`let retries = ${this.options.retries};`));
lines.push(this.indent('let lastError;'));
lines.push('');
lines.push(this.indent('while (retries >= 0) {'));
this.indentLevel++;
lines.push(this.indent('try {'));
this.indentLevel++;
}
// Generate the actual fetch call
const endpoint = `${this.options.endpoint}/${metadata.componentName}/${metadata.functionName}`;
lines.push(this.indent(`const response = await fetch("${endpoint}", requestConfig);`));
if (this.options.timeout && this.options.timeout > 0) {
lines.push(this.indent('clearTimeout(timeoutId);'));
}
// Handle response
lines.push('');
lines.push(this.indent('// Handle response'));
lines.push(this.indent('if (!response.ok) {'));
this.indentLevel++;
if (this.options.errorHandling === 'throw') {
lines.push(this.indent('const errorText = await response.text();'));
lines.push(this.indent('throw new Error(`RPC call failed: ${response.status} ${response.statusText}. ${errorText}`);'));
}
else if (this.options.errorHandling === 'return-null') {
lines.push(this.indent('console.error(`RPC call failed: ${response.status} ${response.statusText}`);'));
lines.push(this.indent('return null;'));
}
else if (this.options.errorHandling === 'return-error') {
lines.push(this.indent('const errorText = await response.text();'));
lines.push(this.indent('return { error: true, status: response.status, message: errorText };'));
}
this.indentLevel--;
lines.push(this.indent('}'));
lines.push('');
// Parse and return response
lines.push(this.indent('// Parse response'));
lines.push(this.indent('const result = await response.json();'));
lines.push(this.indent('return result;'));
// Close retry logic if enabled
if (this.options.retries && this.options.retries > 0) {
this.indentLevel--;
lines.push(this.indent('} catch (error) {'));
this.indentLevel++;
lines.push(this.indent('lastError = error;'));
lines.push(this.indent('retries--;'));
lines.push(this.indent('if (retries < 0) throw lastError;'));
lines.push(this.indent('// Wait before retry'));
lines.push(this.indent('await new Promise(resolve => setTimeout(resolve, 1000 * (3 - retries)));'));
this.indentLevel--;
lines.push(this.indent('}'));
this.indentLevel--;
lines.push(this.indent('}'));
}
this.indentLevel--;
lines.push('}');
return lines.join('\n');
}
/**
* Generate server endpoint path
*/
generateServerEndpoint(metadata) {
return `${this.options.endpoint}/${metadata.componentName}/${metadata.functionName}`;
}
/**
* Generate Express.js route handler for server function
*/
generateServerRouteHandler(metadata) {
this.indentLevel = 0;
const lines = [];
const endpoint = this.generateServerEndpoint(metadata);
lines.push(`// RPC endpoint for ${metadata.componentName}.${metadata.functionName}`);
lines.push(`app.post('${endpoint}', async (req, res) => {`);
this.indentLevel++;
// Add middleware checks
if (metadata.middleware.length > 0) {
lines.push(this.indent('// Apply middleware'));
for (const middleware of metadata.middleware) {
lines.push(this.indent(`await ${middleware}(req, res);`));
}
lines.push('');
}
// Add permission checks
if (metadata.permissions.length > 0) {
lines.push(this.indent('// Check permissions'));
lines.push(this.indent('const userPermissions = req.user?.permissions || [];'));
lines.push(this.indent(`const requiredPermissions = ${JSON.stringify(metadata.permissions)};`));
lines.push(this.indent('const hasPermission = requiredPermissions.every(perm => userPermissions.includes(perm));'));
lines.push(this.indent('if (!hasPermission) {'));
this.indentLevel++;
lines.push(this.indent('return res.status(403).json({ error: "Insufficient permissions" });'));
this.indentLevel--;
lines.push(this.indent('}'));
lines.push('');
}
// Extract parameters from request body
lines.push(this.indent('try {'));
this.indentLevel++;
if (metadata.parameters.length > 0) {
lines.push(this.indent('// Extract parameters'));
const paramNames = metadata.parameters.map(p => p.name);
lines.push(this.indent(`const { ${paramNames.join(', ')} } = req.body;`));
lines.push('');
// Validate required parameters
for (const param of metadata.parameters) {
if (!param.isOptional) {
lines.push(this.indent(`if (${param.name} === undefined || ${param.name} === null) {`));
this.indentLevel++;
lines.push(this.indent(`return res.status(400).json({ error: "Missing required parameter: ${param.name}" });`));
this.indentLevel--;
lines.push(this.indent('}'));
}
}
lines.push('');
}
// Call the actual server function
lines.push(this.indent('// Call server function'));
const paramString = metadata.parameters.map(p => p.name).join(', ');
const awaitKeyword = metadata.isAsync ? 'await ' : '';
lines.push(this.indent(`const result = ${awaitKeyword}${metadata.componentName}Server.${metadata.functionName}(${paramString});`));
lines.push('');
// Return result
lines.push(this.indent('// Return result'));
lines.push(this.indent('res.json(result);'));
// Error handling
this.indentLevel--;
lines.push(this.indent('} catch (error) {'));
this.indentLevel++;
lines.push(this.indent('console.error(`RPC call error in ${metadata.componentName}.${metadata.functionName}:`, error);'));
lines.push(this.indent('res.status(500).json({ error: "Internal server error", message: error.message });'));
this.indentLevel--;
lines.push(this.indent('}'));
this.indentLevel--;
lines.push('});');
return lines.join('\n');
}
/**
* Generate complete RPC client module
*/
generateRPCClientModule(stubs, componentName) {
this.indentLevel = 0;
const lines = [];
lines.push(`// Auto-generated RPC client for ${componentName}`);
lines.push(`// Generated at: ${new Date().toISOString()}`);
lines.push('');
// Add authentication helper if needed
if (this.options.authentication) {
lines.push('// Authentication helper');
lines.push('function getAuthToken() {');
this.indentLevel++;
lines.push(this.indent('// Implement your authentication token retrieval logic here'));
lines.push(this.indent('return localStorage.getItem("authToken") || sessionStorage.getItem("authToken");'));
this.indentLevel--;
lines.push('}');
lines.push('');
}
// Generate RPC client class
lines.push(`export class ${componentName}RPCClient {`);
this.indentLevel++;
// Add constructor
lines.push(this.indent('constructor(options = {}) {'));
this.indentLevel++;
lines.push(this.indent('this.options = { ...options };'));
this.indentLevel--;
lines.push(this.indent('}'));
lines.push('');
// Add each RPC stub as a method
for (const stub of stubs) {
lines.push(this.indent(`// ${stub.metadata.functionName}`));
const stubLines = stub.clientCode.split('\n');
for (const stubLine of stubLines) {
lines.push(this.indent(stubLine));
}
lines.push('');
}
this.indentLevel--;
lines.push('}');
lines.push('');
// Export default instance
lines.push(`export default new ${componentName}RPCClient();`);
return lines.join('\n');
}
/**
* Generate complete server routes module
*/
generateServerRoutesModule(stubs, componentName) {
this.indentLevel = 0;
const lines = [];
lines.push(`// Auto-generated RPC server routes for ${componentName}`);
lines.push(`// Generated at: ${new Date().toISOString()}`);
lines.push('');
lines.push(`const ${componentName}Server = require('./${componentName.toLowerCase()}-server');`);
lines.push('');
lines.push('module.exports = function(app) {');
this.indentLevel++;
for (const stub of stubs) {
const routeHandler = this.generateServerRouteHandler(stub.metadata);
const handlerLines = routeHandler.split('\n');
for (const handlerLine of handlerLines) {
lines.push(this.indent(handlerLine));
}
lines.push('');
}
this.indentLevel--;
lines.push('};');
return lines.join('\n');
}
/**
* Generate parameter string for function signature
*/
generateParameterString(parameters) {
return parameters.map(param => {
let result = param.name;
if (param.isOptional) {
result += '?';
}
if (param.defaultValue) {
result += ` = ${this.generateDefaultValue(param.defaultValue)}`;
}
return result;
}).join(', ');
}
/**
* Generate default value for parameter
*/
generateDefaultValue(defaultValue) {
if (typeof defaultValue === 'string') {
return `"${defaultValue}"`;
}
else if (typeof defaultValue === 'object' && defaultValue.expressionType === 'LITERAL') {
if (typeof defaultValue.value === 'string') {
return `"${defaultValue.value}"`;
}
return String(defaultValue.value);
}
return 'undefined';
}
/**
* Helper to generate indentation
*/
indent(text) {
return ' '.repeat(this.indentLevel) + text;
}
}
//# sourceMappingURL=rpc-generator.js.map