toOpenApiSpec method

Map<String, dynamic> toOpenApiSpec({
  1. String title = 'Bloom API',
  2. String version = '1.0.0',
  3. String description = 'Full-stack Bloom Server API',
})

Generates an OpenAPI 3.1 specification map from all registered routes.

Converts parameterized path patterns (/api/tasks/:id -> /api/tasks/{id}), extracts path parameters, infers tags from URL segments, and configures JSON request/response schemas.

Returns a standard OpenAPI 3.1.0 compliant Map.

Implementation

Map<String, dynamic> toOpenApiSpec({
  String title = 'Bloom API',
  String version = '1.0.0',
  String description = 'Full-stack Bloom Server API',
}) {
  final paths = <String, Map<String, dynamic>>{};

  for (final route in _routes) {
    if (route.method == '*' || route.pathPattern.isEmpty) continue;
    if (route.pathPattern.startsWith('/api/openapi') ||
        route.pathPattern.startsWith('/api/docs') ||
        route.pathPattern.startsWith('/api/swagger') ||
        route.pathPattern == '/' ||
        route.pathPattern.endsWith('.js') ||
        route.pathPattern.endsWith('.json')) {
      continue;
    }

    // Convert /api/tasks/:id to /api/tasks/{id}
    var openApiPath = route.pathPattern;
    for (final param in route.paramNames) {
      openApiPath = openApiPath.replaceAll(':$param', '{$param}');
    }

    final methodLower = route.method.toLowerCase();
    paths.putIfAbsent(openApiPath, () => <String, dynamic>{});

    // Derive tag from path segment (e.g. /api/tasks -> Tasks)
    final segments = openApiPath
        .split('/')
        .where((s) => s.isNotEmpty && s != 'api')
        .toList();
    final tag = segments.isNotEmpty
        ? segments.first.substring(0, 1).toUpperCase() +
            segments.first.substring(1)
        : 'General';

    final operation = <String, dynamic>{
      'tags': [tag],
      'summary': '${route.method} $openApiPath',
      'responses': {
        '200': {'description': 'Successful response'},
      },
    };

    if (route.paramNames.isNotEmpty) {
      operation['parameters'] = route.paramNames
          .map((p) => {
                'name': p,
                'in': 'path',
                'required': true,
                'schema': {'type': 'string'},
              })
          .toList();
    }

    if (methodLower == 'post' ||
        methodLower == 'put' ||
        methodLower == 'patch') {
      operation['requestBody'] = {
        'required': true,
        'content': {
          'application/json': {
            'schema': {'type': 'object'},
          },
        },
      };
    }

    paths[openApiPath]![methodLower] = operation;
  }

  return {
    'openapi': '3.1.0',
    'info': {
      'title': title,
      'version': version,
      'description': description,
    },
    'paths': paths,
  };
}