UNPKG

bowling-analysis-system

Version:

A comprehensive system for analyzing bowling techniques using video processing and metrics calculation

499 lines (426 loc) 14.8 kB
/** * Configuration Manager * * Centralized configuration system with environment-aware settings, * schema validation, and hierarchical configuration. * @module config/ConfigManager */ const fs = require('fs'); const path = require('path'); /** * @typedef {Object} ConfigOptions * @property {Object} [initialConfig] - Initial configuration object * @property {string} [environment] - Environment name * @property {string} [configDir] - Directory for configuration files * @property {boolean} [validateOnSet=true] - Whether to validate on setting values */ /** * ConfigManager for centralized configuration management * @class ConfigManager */ class ConfigManager { /** * Create a new ConfigManager * @param {ConfigOptions} options - Configuration options */ constructor(options = {}) { this.config = new Map(); this.environment = options.environment || process.env.NODE_ENV || 'development'; this.configDir = options.configDir; this.validateOnSet = options.validateOnSet !== false; this.environmentOverrides = new Map(); this.configSchema = new Map(); this.listeners = new Map(); // Set initial configuration if provided if (options.initialConfig) { this.setMultiple(options.initialConfig); } } /** * Set a configuration value * @param {string} key - Configuration key * @param {any} value - Configuration value * @param {Object} [options] - Options * @param {boolean} [options.validate=true] - Whether to validate the value * @param {string} [options.environment] - Specific environment for this setting * @returns {ConfigManager} This instance for chaining */ set(key, value, options = {}) { const shouldValidate = options.validate !== false && this.validateOnSet; const targetEnvironment = options.environment; // Validate via schema if available if (shouldValidate && this.configSchema.has(key)) { const schema = this.configSchema.get(key); // Implementation should use a real validator rather than a mock // Validation logic would be added here } // Store as environment-specific if specified if (targetEnvironment) { if (!this.environmentOverrides.has(targetEnvironment)) { this.environmentOverrides.set(targetEnvironment, new Map()); } this.environmentOverrides.get(targetEnvironment).set(key, value); } else { // Store as default configuration this.config.set(key, value); } // Notify listeners this._notifyListeners(key, value); return this; } /** * Get a configuration value * @param {string} key - Configuration key * @param {any} [defaultValue] - Default value if not found * @returns {any} Configuration value or default */ get(key, defaultValue) { // Check current environment overrides first if (this.environment && this.environmentOverrides.has(this.environment) && this.environmentOverrides.get(this.environment).has(key)) { return this.environmentOverrides.get(this.environment).get(key); } // Check default configuration if (this.config.has(key)) { return this.config.get(key); } // Return default value if provided return defaultValue; } /** * Check if a configuration key exists * @param {string} key - Configuration key * @returns {boolean} True if key exists */ has(key) { return ( this.config.has(key) || (this.environment && this.environmentOverrides.has(this.environment) && this.environmentOverrides.get(this.environment).has(key)) ); } /** * Delete a configuration value * @param {string} key - Configuration key * @param {Object} [options] - Options * @param {string} [options.environment] - Specific environment to delete from * @returns {boolean} True if value was deleted */ delete(key, options = {}) { const targetEnvironment = options.environment; let deleted = false; if (targetEnvironment) { // Delete from specific environment if (this.environmentOverrides.has(targetEnvironment)) { deleted = this.environmentOverrides.get(targetEnvironment).delete(key); } } else { // Delete from default configuration deleted = this.config.delete(key); // Also delete from all environments if requested if (options.deleteFromAllEnvironments) { for (const [env, envConfig] of this.environmentOverrides.entries()) { deleted = envConfig.delete(key) || deleted; } } } if (deleted) { this._notifyListeners(key, undefined, true); } return deleted; } /** * Set multiple configuration values * @param {Object} values - Object with key-value pairs * @param {Object} [options] - Options * @param {boolean} [options.validate=true] - Whether to validate values * @param {string} [options.environment] - Specific environment for these settings * @returns {ConfigManager} This instance for chaining */ setMultiple(values, options = {}) { if (!values || typeof values !== 'object') { return this; } for (const [key, value] of Object.entries(values)) { this.set(key, value, options); } return this; } /** * Get multiple configuration values * @param {string[]} keys - Array of keys to retrieve * @returns {Object} Object with requested key-value pairs */ getMultiple(keys) { const result = {}; for (const key of keys) { result[key] = this.get(key); } return result; } /** * Get all configuration for the current environment * @returns {Object} All configuration as an object */ getAll() { const result = {}; // Add default configuration for (const [key, value] of this.config.entries()) { result[key] = value; } // Override with environment-specific values if (this.environment && this.environmentOverrides.has(this.environment)) { for (const [key, value] of this.environmentOverrides.get(this.environment).entries()) { result[key] = value; } } return result; } /** * Set the current environment * @param {string} environment - Environment name * @returns {ConfigManager} This instance for chaining */ setEnvironment(environment) { this.environment = environment; return this; } /** * Get the current environment * @returns {string} Current environment name */ getEnvironment() { return this.environment; } /** * Register a schema for configuration validation * @param {string} key - Configuration key * @param {Object} schema - JSON Schema object * @returns {ConfigManager} This instance for chaining */ registerSchema(key, schema) { this.configSchema.set(key, schema); return this; } /** * Load configuration from a file * @param {string} filePath - Path to configuration file * @param {Object} [options] - Options * @param {boolean} [options.merge=true] - Whether to merge with existing config * @param {string} [options.environment] - Load as environment-specific config * @returns {ConfigManager} This instance for chaining */ loadFromFile(filePath, options = {}) { try { // Resolve path if only filename provided const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(this.configDir || process.cwd(), filePath); if (!fs.existsSync(resolvedPath)) { throw new Error(`Configuration file not found: ${resolvedPath}`); } const fileContent = fs.readFileSync(resolvedPath, 'utf8'); let parsedConfig; if (resolvedPath.endsWith('.json')) { parsedConfig = JSON.parse(fileContent); } else if (resolvedPath.endsWith('.js')) { // For .js files, require the module // Clear require cache to ensure fresh load delete require.cache[require.resolve(resolvedPath)]; parsedConfig = require(resolvedPath); } else { throw new Error(`Unsupported configuration file format: ${resolvedPath}`); } if (options.merge !== false) { // Merge with existing configuration this.setMultiple(parsedConfig, { environment: options.environment }); } else { // Replace existing configuration if (options.environment) { this.environmentOverrides.set(options.environment, new Map()); for (const [key, value] of Object.entries(parsedConfig)) { this.environmentOverrides.get(options.environment).set(key, value); } } else { this.config.clear(); this.setMultiple(parsedConfig); } } return this; } catch (error) { throw new Error(`Error loading configuration from ${filePath}: ${error.message}`); } } /** * Save configuration to a file * @param {string} filePath - Path to save configuration * @param {Object} [options] - Options * @param {boolean} [options.pretty=true] - Whether to use pretty formatting * @param {string} [options.environment] - Save only specific environment * @returns {ConfigManager} This instance for chaining */ saveToFile(filePath, options = {}) { try { // Resolve path if only filename provided const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(this.configDir || process.cwd(), filePath); let configToSave; if (options.environment) { // Save only specific environment configToSave = {}; if (this.environmentOverrides.has(options.environment)) { for (const [key, value] of this.environmentOverrides.get(options.environment).entries()) { configToSave[key] = value; } } } else { // Save all configuration configToSave = this.getAll(); } const jsonString = JSON.stringify( configToSave, null, options.pretty !== false ? 2 : 0 ); fs.writeFileSync(resolvedPath, jsonString, 'utf8'); return this; } catch (error) { throw new Error(`Error saving configuration to ${filePath}: ${error.message}`); } } /** * Get a nested configuration value using dot notation * @param {string} path - Dot-separated path * @param {any} [defaultValue] - Default value if not found * @returns {any} Configuration value or default */ getPath(path, defaultValue) { const parts = path.split('.'); const key = parts[0]; if (parts.length === 1) { return this.get(key, defaultValue); } const value = this.get(key); if (value === undefined || value === null) { return defaultValue; } let current = value; for (let i = 1; i < parts.length; i++) { if (current === undefined || current === null || typeof current !== 'object') { return defaultValue; } current = current[parts[i]]; } return current !== undefined ? current : defaultValue; } /** * Set a nested configuration value using dot notation * @param {string} path - Dot-separated path * @param {any} value - Value to set * @param {Object} [options] - Options * @returns {ConfigManager} This instance for chaining */ setPath(path, value, options = {}) { const parts = path.split('.'); const key = parts[0]; if (parts.length === 1) { return this.set(key, value, options); } // Get existing value or create new object let current = this.get(key, {}); if (typeof current !== 'object' || current === null) { current = {}; } // Clone to avoid modifying the original const rootObj = { ...current }; let currentObj = rootObj; // Build nested structure for (let i = 1; i < parts.length - 1; i++) { const part = parts[i]; if (!currentObj[part] || typeof currentObj[part] !== 'object') { currentObj[part] = {}; } currentObj = currentObj[part]; } // Set the value at the final level currentObj[parts[parts.length - 1]] = value; // Save the updated object return this.set(key, rootObj, options); } /** * Subscribe to configuration changes * @param {string|Function} keyOrCallback - Configuration key or callback function * @param {Function} [callback] - Callback function if key is provided * @returns {Function} Unsubscribe function */ subscribe(keyOrCallback, callback) { if (typeof keyOrCallback === 'function') { // Global subscription to all changes const id = Symbol('global'); if (!this.listeners.has('*')) { this.listeners.set('*', new Map()); } this.listeners.get('*').set(id, keyOrCallback); return () => { if (this.listeners.has('*')) { this.listeners.get('*').delete(id); } }; } else { // Subscription to specific key const key = keyOrCallback; if (!this.listeners.has(key)) { this.listeners.set(key, new Map()); } const id = Symbol(key); this.listeners.get(key).set(id, callback); return () => { if (this.listeners.has(key)) { this.listeners.get(key).delete(id); } }; } } /** * Notify listeners of configuration changes * @param {string} key - Changed configuration key * @param {any} value - New configuration value * @param {boolean} [isDeleted=false] - Whether the key was deleted * @private */ _notifyListeners(key, value, isDeleted = false) { // Notify key-specific listeners if (this.listeners.has(key)) { for (const callback of this.listeners.get(key).values()) { callback(value, key, isDeleted); } } // Notify global listeners if (this.listeners.has('*')) { for (const callback of this.listeners.get('*').values()) { callback(value, key, isDeleted); } } } /** * Reset configuration * @param {Object} [options] - Options * @param {boolean} [options.resetEnvironments=true] - Whether to reset environment-specific configuration * @param {boolean} [options.resetSchema=false] - Whether to reset schema definitions * @returns {ConfigManager} This instance for chaining */ reset(options = {}) { this.config.clear(); if (options.resetEnvironments !== false) { this.environmentOverrides.clear(); } if (options.resetSchema) { this.configSchema.clear(); } return this; } } module.exports = { ConfigManager };