generate static method
Generates the .openapi.dart REST server source code.
tools — tool definitions from the builder (with sourceImport, sourceAlias, etc.)
port — HTTP port to bind to
address — HTTP bind address
openApiSpec — the already-built OpenAPI 3.0 spec map
logErrors — when true, writes detailed exceptions + stack traces to
stderr while still returning a generic 500 body to the client.
Mirrors @Server(logErrors:) behavior in the MCP templates.
Implementation
static String generate(
List<Map<String, dynamic>> tools,
int port,
String address,
Map<String, dynamic> openApiSpec, {
bool logErrors = false,
}) {
// ── Source imports (aliased, same pattern as HttpTemplate) ──────────
final sourceImports = <String, String>{};
for (final tool in tools) {
final sourceImport = tool['sourceImport'] as String?;
final sourceAlias = tool['sourceAlias'] as String?;
if (sourceImport != null && sourceAlias != null) {
sourceImports[sourceImport] = sourceAlias;
}
}
final sourceImportStatements = sourceImports.entries
.map((e) => "import '${e.key}' as ${e.value};")
.join('\n');
// ── Build a name→tool lookup ───────────────────────────────────────
final toolByName = <String, Map<String, dynamic>>{};
for (final tool in tools) {
toolByName[tool['name'] as String] = tool;
}
// ── Parse openApiSpec paths to build route list ────────────────────
final paths = openApiSpec['paths'] as Map<String, dynamic>? ?? {};
// We collect route registrations and handler functions.
// Routes are split into two buckets so we can register fixed-segment
// routes (like /users/search) before parameterized ones (/users/<id>).
final fixedRoutes = <String>[];
final paramRoutes = <String>[];
final handlers = <String>[];
final handlerNames = <String>{};
for (final pathEntry in paths.entries) {
final openApiPath = pathEntry.key;
final methods = pathEntry.value as Map<String, dynamic>;
// Extract resource name from first path segment for unique param naming.
// e.g. /users/{id} → resource = 'users', /todos/{id} → resource = 'todos'
final segments = openApiPath
.split('/')
.where((s) => s.isNotEmpty && !s.startsWith('{'))
.toList();
final resource = segments.isNotEmpty ? segments.first : 'item';
// Convert {param} → <resourceParam> for shelf_router to ensure
// unique parameter names across all routes (shelf_router requirement).
final paramRenames = <String, String>{};
final shelfPath = openApiPath.replaceAllMapped(RegExp(r'\{(\w+)\}'), (m) {
final origName = m.group(1)!;
final uniqueName =
'$resource${origName[0].toUpperCase()}${origName.substring(1)}';
paramRenames[origName] = uniqueName;
return '<$uniqueName>';
});
final isParameterized = shelfPath.contains('<');
for (final methodEntry in methods.entries) {
final httpMethod = methodEntry.key; // get, post, patch, delete
final operation = methodEntry.value as Map<String, dynamic>;
final operationId = operation['operationId'] as String?;
if (operationId == null) continue;
final lookupKey = (operation['x-tool-name'] as String?) ?? operationId;
final tool = toolByName[lookupKey];
if (tool == null) continue;
// Build a unique handler function name
final handlerFnName = '_handle_${httpMethod}_${_sanitize(operationId)}';
if (handlerNames.contains(handlerFnName)) continue;
handlerNames.add(handlerFnName);
// Route registration
final routeLine =
" app.${httpMethod.toLowerCase()}('$shelfPath', $handlerFnName);";
if (isParameterized) {
paramRoutes.add(routeLine);
} else {
fixedRoutes.add(routeLine);
}
// Build handler
handlers.add(
_buildHandler(
handlerFnName: handlerFnName,
httpMethod: httpMethod.toUpperCase(),
tool: tool,
operation: operation,
openApiPath: openApiPath,
paramRenames: paramRenames,
),
);
}
}
// ── Address expression ───────────────────────────────────────────
final addressExpression = "'${_escapeDartString(address)}'";
// ── Assemble generated code ────────────────────────────────────────
final allRoutes = [...fixedRoutes, ...paramRoutes].join('\n');
return '''
// AUTO-GENERATED by easy_api_generator — DO NOT EDIT
import 'dart:async';
import 'dart:convert';
import 'dart:io' as io;
import 'package:shelf_plus/shelf_plus.dart';
$sourceImportStatements
/// When true, detailed exceptions and stack traces are written to stderr.
/// User-facing 500 responses remain generic regardless of this flag.
const bool _logErrors = $logErrors;
void main() => shelfRun(init, defaultBindPort: $port, defaultBindAddress: $addressExpression);
Handler init() {
var app = Router().plus;
$allRoutes
return app.call;
}
${handlers.join('\n\n')}
dynamic _serializeResult(dynamic result) {
if (result == null) return null;
try {
if (result is Map) return result;
if (result is List) {
return result.map((e) {
if (e == null) return null;
if (e is Map) return e;
try {
final toJson = e.toJson;
if (toJson != null && toJson is Function) return toJson();
} catch (_) {}
return e.toString();
}).toList();
}
final toJson = result.toJson;
if (toJson != null && toJson is Function) return toJson();
return result.toString();
} catch (_) {
return result.toString();
}
}
''';
}