import { normalizeCategory, normalizeLang, sanitizeBlacklist, SUPPORTED_CATEGORIES, SUPPORTED_LANGS, type Category, type Lang } from './validation.js';
export type PrimaryProvider = 'jokeapi' | 'official';
export interface EnvLike {
[key: string]: unknown;
}
export interface RuntimeConfig {
provider: PrimaryProvider;
defaultCategory: Category;
lang: Lang;
timeoutMs: number;
retries: number;
allowNet: boolean;
verbose: boolean;
supportedCategories: readonly Category[];
supportedLangs: readonly Lang[];
sanitizeBlacklist: typeof sanitizeBlacklist;
}
const DEFAULTS: RuntimeConfig = {
provider: 'jokeapi',
defaultCategory: 'programming',
lang: 'en',
timeoutMs: 2000,
retries: 2,
allowNet: false,
verbose: false,
supportedCategories: SUPPORTED_CATEGORIES,
supportedLangs: SUPPORTED_LANGS,
sanitizeBlacklist,
};
export function loadConfig(env: EnvLike = process.env): RuntimeConfig {
const provider = normalizeProvider(env.JOKE_API ?? DEFAULTS.provider);
const defaultCategory = normalizeCategory(env.JOKE_DEFAULT_CATEGORY, DEFAULTS.defaultCategory);
const lang = normalizeLang(env.JOKE_LANG, DEFAULTS.lang);
const timeoutMs = parsePositiveInt(env.TIMEOUT_MS, DEFAULTS.timeoutMs, 'TIMEOUT_MS');
const retries = parseNonNegativeInt(env.RETRIES, DEFAULTS.retries, 'RETRIES');
const allowNet = parseBoolean(env.ALLOW_NET, DEFAULTS.allowNet);
const verbose = parseBoolean(env.LOG_VERBOSE, DEFAULTS.verbose);
return {
provider,
defaultCategory,
lang,
timeoutMs,
retries,
allowNet,
verbose,
supportedCategories: SUPPORTED_CATEGORIES,
supportedLangs: SUPPORTED_LANGS,
sanitizeBlacklist,
};
}
function normalizeProvider(value: unknown): PrimaryProvider {
if (!value) return DEFAULTS.provider;
const normalized = String(value).trim().toLowerCase();
if (normalized !== 'jokeapi' && normalized !== 'official') {
throw new Error(`Unsupported JOKE_API provider: ${String(value)}`);
}
return normalized;
}
function parsePositiveInt(value: unknown, fallback: number, field: string): number {
if (value === undefined || value === null || value === '') return fallback;
const parsed = Number.parseInt(String(value), 10);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new Error(`${field} must be a positive integer`);
}
return parsed;
}
function parseNonNegativeInt(value: unknown, fallback: number, field: string): number {
if (value === undefined || value === null || value === '') return fallback;
const parsed = Number.parseInt(String(value), 10);
if (!Number.isFinite(parsed) || parsed < 0) {
throw new Error(`${field} must be a non-negative integer`);
}
return parsed;
}
function parseBoolean(value: unknown, fallback: boolean): boolean {
if (value === undefined || value === null || value === '') return fallback;
const normalized = String(value).trim().toLowerCase();
if (normalized === 'true' || normalized === '1') return true;
if (normalized === 'false' || normalized === '0') return false;
throw new Error(`Invalid boolean value: ${String(value)}`);
}