/**
* Swagger Loader Utility
* Loads Swagger definition from SWAGGER_URL environment variable
* Downloads and caches the file if needed
*
* [Multi-API Support] apiName 파라미터로 다중 API 지원
*/
import axios from 'axios';
import crypto from 'crypto';
import dotenv from 'dotenv';
import fs from 'fs';
import yaml from 'js-yaml';
import path from 'path';
import { getAllApis, getApiConfig, type ApiConfig } from './apiConfigLoader.js';
import logger from './logger.js';
// Load environment variables
dotenv.config();
// Cache directory for swagger files
const SWAGGER_CACHE_DIR = path.join(process.cwd(), 'swagger-cache');
// Ensure cache directory exists (handle race conditions safely)
try {
fs.mkdirSync(SWAGGER_CACHE_DIR, { recursive: true });
} catch (err: any) {
if (err.code !== 'EEXIST') {
throw err;
}
}
/**
* Gets the Swagger URL from CLI argument (highest priority)
* @returns The Swagger URL or null if not set
*/
export function getSwaggerUrlFromCLI(): string | null {
const url = process.env.SWAGGER_URL_FROM_CLI;
if (!url) {
return null;
}
return url.trim();
}
/**
* Gets the Swagger URL for a specific API by name
* @param apiName The name of the API from apis.yaml
* @returns The Swagger URL or null if not found
*/
export function getSwaggerUrlByApiName(apiName: string): string | null {
const apiConfig = getApiConfig(apiName);
if (!apiConfig) {
logger.warn(`API not found in config: ${apiName}`);
return null;
}
return apiConfig.url;
}
/**
* Gets all configured APIs
* @returns Array of API configurations
*/
export function getConfiguredApis(): ApiConfig[] {
return getAllApis();
}
/**
* Gets the cached Swagger file path for a given URL if it exists
* @param swaggerUrl The Swagger URL
* @returns The cached file path or null if not cached
*/
function getCachedSwaggerFilePath(swaggerUrl: string): string | null {
const urlObj = new URL(swaggerUrl);
const filename = crypto
.createHash('sha256')
.update(urlObj.toString())
.digest('hex');
const cacheFilePath = path.join(SWAGGER_CACHE_DIR, `${filename}.json`);
const cacheFilePathYaml = path.join(SWAGGER_CACHE_DIR, `${filename}.yaml`);
// Check if cached file exists
if (fs.existsSync(cacheFilePath)) {
return cacheFilePath;
} else if (fs.existsSync(cacheFilePathYaml)) {
return cacheFilePathYaml;
}
return null;
}
/**
* Downloads and caches a Swagger file from URL
* Handles both JSON and YAML formats
* @param url The Swagger URL
* @returns The path to the cached file
*/
async function downloadAndCacheSwagger(url: string): Promise<string> {
try {
logger.info(`Downloading Swagger definition from ${url}`);
const response = await axios.get(url, {
responseType: 'text', // Get raw text to handle both JSON and YAML
headers: {
Accept: 'application/json, application/yaml, text/yaml',
},
});
// Try to parse as JSON first, then YAML if JSON fails
let swaggerData: any;
let isYaml = false;
try {
swaggerData = JSON.parse(response.data);
} catch (jsonErr) {
try {
swaggerData = yaml.load(response.data);
isYaml = true;
} catch (yamlErr) {
throw new Error('Response is neither valid JSON nor valid YAML');
}
}
if (!swaggerData.openapi && !swaggerData.swagger) {
throw new Error(
'Invalid Swagger definition: missing required "openapi" or "swagger" field'
);
}
// Generate cache filename based on URL hash
const urlObj = new URL(url);
const filename = crypto
.createHash('sha256')
.update(urlObj.toString())
.digest('hex');
const fileExtension = isYaml ? '.yaml' : '.json';
const filePath = path.join(
SWAGGER_CACHE_DIR,
`${filename}${fileExtension}`
);
// Save the file
if (isYaml) {
fs.writeFileSync(filePath, response.data, 'utf8');
} else {
fs.writeFileSync(filePath, JSON.stringify(swaggerData, null, 2), 'utf8');
}
logger.info(`Swagger definition cached at ${filePath}`);
return filePath;
} catch (error: any) {
logger.error(`Failed to download Swagger definition: ${error.message}`);
throw new Error(
`Failed to download Swagger definition from ${url}: ${error.message}`
);
}
}
/**
* Loads Swagger definition from file path
* @param swaggerFilePath Path to the Swagger file
* @returns The Swagger definition object
*/
export async function loadSwaggerDefinitionFromFile(
swaggerFilePath: string
): Promise<any> {
if (!fs.existsSync(swaggerFilePath)) {
throw new Error(`Swagger file not found at ${swaggerFilePath}`);
}
logger.info(`Reading Swagger definition from ${swaggerFilePath}`);
const swaggerContent = fs.readFileSync(swaggerFilePath, 'utf8');
// Parse based on file extension
if (swaggerFilePath.endsWith('.yml') || swaggerFilePath.endsWith('.yaml')) {
return yaml.load(swaggerContent);
} else {
return JSON.parse(swaggerContent);
}
}
/**
* Loads Swagger definition content from URL or cache
* Priority: apiName > CLI --swagger-url > swaggerFilePath parameter
* @param swaggerFilePath Optional file path to Swagger file (used if no CLI arg)
* @param apiName Optional API name from apis.yaml (highest priority)
* @returns The Swagger definition object
*/
export async function loadSwaggerDefinition(
swaggerFilePath?: string,
apiName?: string
): Promise<any> {
// Check apiName first (highest priority for multi-API support)
if (apiName) {
const apiUrl = getSwaggerUrlByApiName(apiName);
if (apiUrl) {
logger.info(`Using Swagger URL from API config (${apiName}): ${apiUrl}`);
return await loadSwaggerDefinitionFromUrl(apiUrl);
}
throw new Error(
`API not found in configuration: ${apiName}. Check apis.yaml file.`
);
}
// Check CLI argument (second priority)
const swaggerUrlFromCLI = getSwaggerUrlFromCLI();
if (swaggerUrlFromCLI) {
logger.info(`Using Swagger URL from CLI: ${swaggerUrlFromCLI}`);
return await loadSwaggerDefinitionFromUrl(swaggerUrlFromCLI);
}
// Check swaggerFilePath parameter (fallback)
if (swaggerFilePath) {
logger.info(`Using Swagger file path: ${swaggerFilePath}`);
return await loadSwaggerDefinitionFromFile(swaggerFilePath);
}
// If none provided, throw error
throw new Error(
'API name, Swagger URL, or file path is required. Provide apiName parameter, --swagger-url=<url> as CLI argument, or swaggerFilePath parameter.'
);
}
/**
* Loads Swagger definition from URL (downloads and caches if needed)
* @param swaggerUrl The Swagger URL
* @returns The Swagger definition object
*/
async function loadSwaggerDefinitionFromUrl(swaggerUrl: string): Promise<any> {
// Check if cached file exists using shared helper
let cachedFilePath = getCachedSwaggerFilePath(swaggerUrl);
// If not cached, download it
if (!cachedFilePath) {
logger.info(
`Swagger definition not found in cache, downloading from ${swaggerUrl}`
);
cachedFilePath = await downloadAndCacheSwagger(swaggerUrl);
} else {
logger.info(`Using cached Swagger definition from ${cachedFilePath}`);
}
// Read and parse the file
return await loadSwaggerDefinitionFromFile(cachedFilePath);
}
/**
* Gets the cached file path for the Swagger definition
* Downloads it if not cached
* Priority: apiName > CLI --swagger-url > swaggerFilePath parameter
* @param swaggerFilePath Optional file path to Swagger file (used if no CLI arg)
* @param apiName Optional API name from apis.yaml (highest priority)
* @returns The path to the Swagger file
*/
export async function getSwaggerFilePath(
swaggerFilePath?: string,
apiName?: string
): Promise<string> {
// Check apiName first (highest priority for multi-API support)
if (apiName) {
const apiUrl = getSwaggerUrlByApiName(apiName);
if (apiUrl) {
return await getSwaggerFilePathFromUrl(apiUrl);
}
throw new Error(
`API not found in configuration: ${apiName}. Check apis.yaml file.`
);
}
// Check CLI argument (second priority)
const swaggerUrlFromCLI = getSwaggerUrlFromCLI();
if (swaggerUrlFromCLI) {
return await getSwaggerFilePathFromUrl(swaggerUrlFromCLI);
}
// Check swaggerFilePath parameter (fallback)
if (swaggerFilePath) {
if (!fs.existsSync(swaggerFilePath)) {
throw new Error(`Swagger file not found at ${swaggerFilePath}`);
}
return swaggerFilePath;
}
// If none provided, throw error
throw new Error(
'API name, Swagger URL, or file path is required. Provide apiName parameter, --swagger-url=<url> as CLI argument, or swaggerFilePath parameter.'
);
}
/**
* Helper function to get swagger file path from URL
* @param url The Swagger URL
* @returns The path to the cached Swagger file
*/
async function getSwaggerFilePathFromUrl(url: string): Promise<string> {
// Generate cache filename based on URL hash
const urlObj = new URL(url);
const filename = crypto
.createHash('sha256')
.update(urlObj.toString())
.digest('hex');
const cacheFilePath = path.join(SWAGGER_CACHE_DIR, `${filename}.json`);
const cacheFilePathYaml = path.join(SWAGGER_CACHE_DIR, `${filename}.yaml`);
// Check if cached file exists
if (fs.existsSync(cacheFilePath)) {
return cacheFilePath;
} else if (fs.existsSync(cacheFilePathYaml)) {
return cacheFilePathYaml;
}
// If not cached, download it
logger.info(`Swagger definition not found in cache, downloading from ${url}`);
return await downloadAndCacheSwagger(url);
}