All files / src/core/asyncapi generate.ts

100% Statements 17/17
92.85% Branches 13/14
100% Functions 3/3
100% Lines 17/17

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107                                            3x             7x 7x                     10x 10x   10x 7x   7x     7x                                     10x 10x                                 10x                       2x 2x 2x 1x   2x    
/**
 * AsyncAPI specification generation for WebSocket endpoints
 * Core logic - no Vite dependencies
 */
 
import * as fs from 'fs';
import * as path from 'path';
 
export type SocketForDocs = { pattern: string };
 
export interface AsyncApiInfo {
  title?: string;
  version?: string;
  description?: string;
}
 
export interface AsyncApiOptions {
  info?: AsyncApiInfo;
  /** Server URL for WebSocket (e.g. ws://localhost:5173). Defaults to current origin with ws(s). */
  serverUrl?: string;
}
 
const DEFAULT_ASYNCAPI_INFO: AsyncApiInfo = {
  title: 'Vitek WebSocket API',
  version: '1.0.0',
  description: 'WebSocket endpoints (auto-generated from socket routes)',
};
 
function fullSocketPath(basePath: string, pattern: string): string {
  const base = basePath.replace(/\/$/, '');
  return pattern === '' ? base : `${base}/${pattern}`;
}
 
/**
 * Generates AsyncAPI 2.x specification from socket entries
 */
export function generateAsyncApiSpec(
  sockets: SocketForDocs[],
  socketBasePath: string,
  options: AsyncApiOptions = {}
): object {
  const info = { ...DEFAULT_ASYNCAPI_INFO, ...options.info };
  const channels: Record<string, unknown> = {};
 
  for (const socket of sockets) {
    const channelPath = fullSocketPath(socketBasePath, socket.pattern);
    const description =
      socket.pattern === ''
        ? 'Root WebSocket endpoint'
        : `WebSocket: ${channelPath}`;
    channels[channelPath] = {
      description,
      subscribe: {
        operationId: `onMessage_${channelPath.replace(/\//g, '_')}`,
        message: {
          description: 'Incoming message',
          payload: { type: 'object', description: 'JSON payload' },
        },
      },
      publish: {
        operationId: `send_${channelPath.replace(/\//g, '_')}`,
        message: {
          description: 'Outgoing message',
          payload: { type: 'object', description: 'JSON payload' },
        },
      },
    };
  }
 
  const serverUrl = options.serverUrl ?? 'ws://localhost:5173';
  const spec: Record<string, unknown> = {
    asyncapi: '2.4.0',
    info: {
      title: info.title,
      version: info.version ?? '1.0.0',
      description: info.description,
    },
    servers: {
      development: {
        url: serverUrl,
        protocol: serverUrl.startsWith('wss') ? 'wss' : 'ws',
        description: 'Development server',
      },
    },
    channels,
  };
 
  return spec;
}
 
/**
 * Writes AsyncAPI spec to a JSON file
 */
export async function generateAsyncApiFile(
  outputPath: string,
  sockets: SocketForDocs[],
  socketBasePath: string,
  options: AsyncApiOptions = {}
): Promise<void> {
  const spec = generateAsyncApiSpec(sockets, socketBasePath, options);
  const dir = path.dirname(outputPath);
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
  fs.writeFileSync(outputPath, JSON.stringify(spec, null, 2), 'utf-8');
}