/** * Configuration management with dot-notation paths and underwrite pattern. * * The Config class manages SDK configuration with support for: * - Dot-notation paths for nested access (`'my.plugin.setting'`) * - Underwrite pattern for defaults (existing values take precedence) * - Required plugin tracking * - Immutable getAll() for safe access * * @example * ```typescript * const config = new Config({ api: { timeout: 5000 } }); * * // Set defaults (won't overwrite existing timeout) * config.defaults({ api: { timeout: 3000, retries: 3 } }); * * // Get with dot-notation * config.get('api.timeout'); // 5000 * config.get('api.retries'); // 3 * * // Set values * config.set('api.baseUrl', 'https://api.example.com'); * ``` */ import { deepMerge } from '../util/deep-merge'; export class Config { private data: Record = {}; private required = new Set(); /** * Create a new Config instance. * * @param initialConfig - Initial configuration object */ constructor(initialConfig: Record = {}) { // Deep copy to prevent external mutation this.data = JSON.parse(JSON.stringify(initialConfig)); } /** * Set default configuration values using underwrite pattern. * * Existing configuration values always take precedence over defaults. * This allows plugins to provide defaults without overwriting user config. * * @param config - Default configuration object * * @example * ```typescript * // User provided config * const config = new Config({ api: { timeout: 10000 } }); * * // Plugin sets defaults * config.defaults({ * api: { timeout: 3000, retries: 3 }, * debug: false * }); * * // Result: { api: { timeout: 10000, retries: 3 }, debug: false } * // User's timeout (10000) wins over default (3000) * ``` */ defaults(config: Record): void { // Use underwrite pattern: existing config (target) wins over defaults (source) this.data = deepMerge(this.data, config); } /** * Merge configuration values (new values override existing). * * New configuration values take precedence over existing values. * This is the opposite of defaults() and is used when user provides new config. * * @param config - Configuration object to merge * * @example * ```typescript * const config = new Config({ api: { timeout: 3000 } }); * * // User provides new config * config.merge({ api: { timeout: 10000, retries: 3 } }); * * // Result: { api: { timeout: 10000, retries: 3 } } * // New timeout (10000) wins over existing (3000) * ``` */ merge(config: Record): void { // Regular merge: new config (source) wins over existing (target) this.data = deepMerge(config, this.data); } /** * Get a configuration value by dot-notation path. * * @param path - Dot-notation path (e.g., 'api.timeout') * @returns Configuration value or undefined if not found * * @example * ```typescript * config.get('api.timeout'); // 5000 * config.get('api.retries'); // 3 * config.get('nonexistent.path'); // undefined * config.get('api.timeout'); // Type-safe access * ``` */ get(path: string): T { const keys = path.split('.'); let current: any = this.data; for (const key of keys) { if (current == null) { return undefined as T; } current = current[key]; } return current as T; } /** * Set a configuration value by dot-notation path. * * Creates intermediate objects as needed. * * @param path - Dot-notation path (e.g., 'api.timeout') * @param value - Value to set * * @example * ```typescript * config.set('api.timeout', 5000); * config.set('api.headers.authorization', 'Bearer token'); * config.set('deeply.nested.value', 42); * ``` */ set(path: string, value: any): void { const keys = path.split('.'); const lastKey = keys.pop(); if (!lastKey) return; // Guard against empty path let current: any = this.data; // Create intermediate objects for (const key of keys) { if (current[key] == null || typeof current[key] !== 'object') { current[key] = {}; } current = current[key]; } current[lastKey] = value; } /** * Mark a plugin namespace as required. * * Required plugins must be properly enabled for the SDK to function. * * @param namespace - Plugin namespace to mark as required * * @example * ```typescript * config.markRequired('analytics'); * config.isRequired('analytics'); // true * ``` */ markRequired(namespace?: string): void { if (namespace !== undefined && namespace !== null) { this.required.add(namespace); } } /** * Check if a plugin namespace is marked as required. * * @param namespace - Plugin namespace to check * @returns True if the namespace is required */ isRequired(namespace: string): boolean { return this.required.has(namespace); } /** * Get all configuration as an immutable copy. * * Returns a deep copy to prevent external mutation of config. * * @returns Copy of the entire configuration object * * @example * ```typescript * const allConfig = config.getAll(); * allConfig.api = {}; // Does not affect internal config * ``` */ getAll(): Record { // Deep copy to prevent nested mutation return JSON.parse(JSON.stringify(this.data)); } }