bowling-analysis-system
Version:
A comprehensive system for analyzing bowling techniques using video processing and metrics calculation
499 lines (426 loc) • 14.8 kB
JavaScript
/**
* 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 };