@snow-tzu/type-config
Version:
Core configuration management system with Spring Boot-like features
142 lines (123 loc) • 4.75 kB
text/typescript
/**
* PlaceholderResolver - Resolves environment variable placeholders in configuration values
*
* Supports syntax: ${ENV_VAR_NAME:fallback}
* - ${VAR} - resolves to environment variable VAR, undefined if not set
* - ${VAR:fallback} - resolves to environment variable VAR, or fallback if not set
* - \${VAR} - escapes to literal ${VAR} (no resolution)
*/
export class PlaceholderResolver {
// Regex pattern to match ${VAR_NAME:fallback} or ${VAR_NAME}
// Captures: group 1 = VAR_NAME, group 2 = fallback (optional)
private readonly placeholderPattern = /\$\{([^}:]+)(?::([^}]*))?\}/g;
// Regex pattern to detect escaped placeholders \${...}
private readonly escapedPattern = /\\\$\{([^}]+)\}/g;
/**
* Check if a string contains placeholder syntax
* @param value - String to check
* @returns true if value contains ${...} pattern
*/
hasPlaceholder(value: string): boolean {
if (typeof value !== 'string') {
return false;
}
// Reset regex state
this.placeholderPattern.lastIndex = 0;
return this.placeholderPattern.test(value);
}
/**
* Resolve all placeholders in a string value
* @param value - String that may contain ${VAR:fallback} patterns
* @param envProvider - Function to get environment variables (defaults to process.env)
* @returns Resolved string with all placeholders replaced, or undefined if resolution fails
*/
resolve(
value: string,
envProvider: (key: string) => string | undefined = (key) => process.env[key]
): string | undefined {
if (typeof value !== 'string') {
return value;
}
// First, handle escaped placeholders by temporarily replacing them
const escapedPlaceholders: string[] = [];
const tempValue = value.replace(this.escapedPattern, (match, content) => {
const placeholder = `__ESCAPED_${escapedPlaceholders.length}__`;
escapedPlaceholders.push(`\${${content}}`);
return placeholder;
});
// Track if we found any placeholders
let foundPlaceholder = false;
let allResolved = true;
// Reset regex state
this.placeholderPattern.lastIndex = 0;
// Resolve actual placeholders
const resolved = tempValue.replace(
this.placeholderPattern,
(match, varName, fallback) => {
foundPlaceholder = true;
const envValue = envProvider(varName.trim());
if (envValue !== undefined) {
// Environment variable exists, use its value
return envValue;
} else if (fallback !== undefined) {
// Environment variable doesn't exist, use fallback (can be empty string)
return fallback;
} else {
// No environment variable and no fallback - mark as unresolved
allResolved = false;
return match; // Keep the original placeholder
}
}
);
// If we found placeholders but couldn't resolve all of them, return undefined
if (foundPlaceholder && !allResolved) {
return undefined;
}
// Restore escaped placeholders (remove backslash)
let finalValue = resolved;
escapedPlaceholders.forEach((escaped, index) => {
finalValue = finalValue.replace(`__ESCAPED_${index}__`, escaped);
});
return finalValue;
}
/**
* Recursively resolve placeholders in an entire configuration object
* @param config - Configuration object with potential placeholders
* @param envProvider - Function to get environment variables
* @returns New object with all placeholders resolved
*/
resolveObject(
config: Record<string, any>,
envProvider: (key: string) => string | undefined = (key) => process.env[key]
): Record<string, any> {
if (!config || typeof config !== 'object') {
return config;
}
if (Array.isArray(config)) {
return config.map((item) => {
if (typeof item === 'string') {
return this.resolve(item, envProvider);
} else if (typeof item === 'object' && item !== null) {
return this.resolveObject(item, envProvider);
}
return item;
});
}
const resolved: Record<string, any> = {};
for (const [key, value] of Object.entries(config)) {
if (typeof value === 'string') {
const resolvedValue = this.resolve(value, envProvider);
// Only set the property if resolution succeeded
// If resolvedValue is undefined, the property will be omitted
if (resolvedValue !== undefined) {
resolved[key] = resolvedValue;
}
} else if (typeof value === 'object' && value !== null) {
resolved[key] = this.resolveObject(value, envProvider);
} else {
resolved[key] = value;
}
}
return resolved;
}
}