generate static method

String generate(
  1. List<Map<String, dynamic>> tools,
  2. int port,
  3. String address,
  4. Map<String, dynamic> openApiSpec, {
  5. bool logErrors = false,
})

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();
}
}
''';
}