modules/Core/SimpleConfigurationManager.psm1
|
# Simple Configuration Manager Module # Provides configuration management without PowerShell classes for better compatibility # Configuration schema as hashtable $script:ConfigurationSchema = @{ liveDisplay = @{ refreshInterval = @{ Type = "int"; Min = 500; Max = 5000; Default = 1000 } displayMode = @{ Type = "string"; ValidValues = @("detailed", "compact", "minimal"); Default = "detailed" } sidecarPosition = @{ Type = "string"; ValidValues = @("left", "right", "top", "bottom"); Default = "right" } sidecarWidth = @{ Type = "int"; Min = 20; Max = 100; Default = 40 } showAlbumArt = @{ Type = "bool"; Default = $true } colorScheme = @{ Type = "string"; ValidValues = @("auto", "light", "dark"); Default = "auto" } useAnsiEscapes = @{ Type = "bool"; Default = $true } } lyrics = @{ preferredProvider = @{ Type = "string"; ValidValues = @("genius", "musixmatch", "mock"); Default = "mock" } cacheEnabled = @{ Type = "bool"; Default = $true } syncHighlighting = @{ Type = "bool"; Default = $true } scrollSpeed = @{ Type = "int"; Min = 1; Max = 10; Default = 3 } cacheDirectory = @{ Type = "string"; Default = "" } maxCacheAgeDays = @{ Type = "int"; Min = 1; Max = 365; Default = 30 } geniusApiKey = @{ Type = "string"; Default = "" } } statistics = @{ trackingEnabled = @{ Type = "bool"; Default = $true } retentionDays = @{ Type = "int"; Min = 30; Max = 1095; Default = 365 } exportFormat = @{ Type = "string"; ValidValues = @("json", "csv"); Default = "json" } defaultPeriod = @{ Type = "string"; ValidValues = @("day", "week", "month", "year"); Default = "month" } dataDirectory = @{ Type = "string"; Default = "" } autoCleanup = @{ Type = "bool"; Default = $true } } apiClient = @{ maxRequestsPerMinute = @{ Type = "int"; Min = 10; Max = 100; Default = 60 } timeoutMs = @{ Type = "int"; Min = 5000; Max = 30000; Default = 10000 } retryAttempts = @{ Type = "int"; Min = 1; Max = 5; Default = 3 } cacheEnabled = @{ Type = "bool"; Default = $true } cacheDurationMs = @{ Type = "int"; Min = 1000; Max = 300000; Default = 60000 } } } # Global configuration state $script:ConfigurationDirectory = $null $script:ConfigurationFile = $null $script:CurrentConfiguration = $null $script:IsLoaded = $false function Initialize-SimpleConfigurationManager { <# .SYNOPSIS Initialize the simple configuration manager #> param( [string]$ConfigDir = (Join-Path $env:APPDATA "SpotifyCLI\LiveFeatures") ) $script:ConfigurationDirectory = $ConfigDir $script:ConfigurationFile = Join-Path $ConfigDir "live_features_config.json" # Ensure directory exists if (-not (Test-Path $script:ConfigurationDirectory)) { New-Item -ItemType Directory -Path $script:ConfigurationDirectory -Force | Out-Null } # Load configuration Load-SimpleConfiguration } function Get-DefaultConfiguration { <# .SYNOPSIS Get the default configuration #> $config = @{} foreach ($section in $script:ConfigurationSchema.Keys) { $config[$section] = @{} foreach ($key in $script:ConfigurationSchema[$section].Keys) { $config[$section][$key] = $script:ConfigurationSchema[$section][$key].Default } } return $config } function Test-ConfigurationValue { <# .SYNOPSIS Validate a configuration value #> param( [string]$Section, [string]$Key, $Value ) if (-not $script:ConfigurationSchema.ContainsKey($Section)) { return $false } $sectionSchema = $script:ConfigurationSchema[$Section] if (-not $sectionSchema.ContainsKey($Key)) { return $false } $keySchema = $sectionSchema[$Key] switch ($keySchema.Type) { "int" { if ($Value -isnot [int]) { try { $Value = [int]$Value } catch { return $false } } if ($keySchema.ContainsKey('Min') -and $Value -lt $keySchema.Min) { return $false } if ($keySchema.ContainsKey('Max') -and $Value -gt $keySchema.Max) { return $false } return $true } "bool" { if ($Value -isnot [bool]) { if ($Value -eq "true" -or $Value -eq "True" -or $Value -eq $true) { return $true } if ($Value -eq "false" -or $Value -eq "False" -or $Value -eq $false) { return $true } return $false } return $true } "string" { if ($Value -isnot [string]) { $Value = [string]$Value } if ($keySchema.ContainsKey('ValidValues') -and $Value -notin $keySchema.ValidValues) { return $false } return $true } default { return $true } } } function Get-ConfigurationValidationErrors { <# .SYNOPSIS Get validation errors for a configuration #> param([hashtable]$Config) $errors = @() foreach ($section in $Config.Keys) { if ($section -eq '_metadata') { continue } # Skip metadata if (-not $script:ConfigurationSchema.ContainsKey($section)) { $errors += "Unknown configuration section: $section" continue } foreach ($key in $Config[$section].Keys) { if (-not $script:ConfigurationSchema[$section].ContainsKey($key)) { $errors += "Unknown configuration key: $section.$key" continue } if (-not (Test-ConfigurationValue -Section $section -Key $key -Value $Config[$section][$key])) { $keySchema = $script:ConfigurationSchema[$section][$key] $errors += "Invalid value for $section.$key : Expected $($keySchema.Type)" if ($keySchema.ContainsKey('ValidValues')) { $errors += " Valid values: $($keySchema.ValidValues -join ', ')" } if ($keySchema.ContainsKey('Min') -and $keySchema.ContainsKey('Max')) { $errors += " Valid range: $($keySchema.Min) - $($keySchema.Max)" } } } } return $errors } function Load-SimpleConfiguration { <# .SYNOPSIS Load configuration from file #> try { if (Test-Path $script:ConfigurationFile) { $json = Get-Content -Path $script:ConfigurationFile -Raw -Encoding UTF8 $loadedConfig = $json | ConvertFrom-Json # Convert PSCustomObject to hashtable $config = ConvertTo-Hashtable -InputObject $loadedConfig # Validate configuration $errors = Get-ConfigurationValidationErrors -Config $config if ($errors.Count -gt 0) { Write-Warning "Configuration validation errors found:" foreach ($error in $errors) { Write-Warning " $error" } Write-Warning "Using default configuration for invalid settings" } # Merge with defaults $script:CurrentConfiguration = Merge-Configurations -Default (Get-DefaultConfiguration) -Override $config } else { # Create default configuration file $script:CurrentConfiguration = Get-DefaultConfiguration Save-SimpleConfiguration } $script:IsLoaded = $true } catch { Write-Warning "Failed to load configuration: $($_.Exception.Message)" Write-Warning "Using default configuration" $script:CurrentConfiguration = Get-DefaultConfiguration $script:IsLoaded = $true } } function ConvertTo-Hashtable { <# .SYNOPSIS Convert PSCustomObject to hashtable recursively #> param($InputObject) if ($InputObject -is [PSCustomObject]) { $hashtable = @{} foreach ($property in $InputObject.PSObject.Properties) { if ($property.Value -is [PSCustomObject]) { $hashtable[$property.Name] = ConvertTo-Hashtable -InputObject $property.Value } else { $hashtable[$property.Name] = $property.Value } } return $hashtable } else { return $InputObject } } function Merge-Configurations { <# .SYNOPSIS Merge two configurations #> param( [hashtable]$Default, [hashtable]$Override ) $merged = $Default.Clone() foreach ($section in $Override.Keys) { if ($section -eq '_metadata') { $merged[$section] = $Override[$section] continue } if ($merged.ContainsKey($section)) { foreach ($key in $Override[$section].Keys) { if ($merged[$section].ContainsKey($key)) { # Validate the override value if (Test-ConfigurationValue -Section $section -Key $key -Value $Override[$section][$key]) { $merged[$section][$key] = $Override[$section][$key] } } } } } return $merged } function Save-SimpleConfiguration { <# .SYNOPSIS Save configuration to file #> try { $json = $script:CurrentConfiguration | ConvertTo-Json -Depth 10 Set-Content -Path $script:ConfigurationFile -Value $json -Encoding UTF8 } catch { Write-Warning "Failed to save configuration: $($_.Exception.Message)" } } function Get-SimpleConfiguration { <# .SYNOPSIS Get the current configuration #> if (-not $script:IsLoaded) { Initialize-SimpleConfigurationManager } return $script:CurrentConfiguration.Clone() } function Get-SimpleConfigurationSection { <# .SYNOPSIS Get a specific configuration section #> param([string]$Section) $config = Get-SimpleConfiguration if ($config.ContainsKey($Section)) { return $config[$Section].Clone() } return @{} } function Get-SimpleConfigurationValue { <# .SYNOPSIS Get a specific configuration value #> param( [string]$Section, [string]$Key ) $sectionConfig = Get-SimpleConfigurationSection -Section $Section if ($sectionConfig.ContainsKey($Key)) { return $sectionConfig[$Key] } # Return default value if not found $defaultConfig = Get-DefaultConfiguration if ($defaultConfig.ContainsKey($Section) -and $defaultConfig[$Section].ContainsKey($Key)) { return $defaultConfig[$Section][$Key] } return $null } function Set-SimpleConfigurationValue { <# .SYNOPSIS Set a specific configuration value #> param( [string]$Section, [string]$Key, $Value ) if (-not $script:IsLoaded) { Initialize-SimpleConfigurationManager } # Validate the value if (-not (Test-ConfigurationValue -Section $Section -Key $Key -Value $Value)) { throw "Invalid value for $Section.$Key" } # Ensure section exists if (-not $script:CurrentConfiguration.ContainsKey($Section)) { $script:CurrentConfiguration[$Section] = @{} } $script:CurrentConfiguration[$Section][$Key] = $Value Save-SimpleConfiguration } function Reset-SimpleConfiguration { <# .SYNOPSIS Reset configuration to defaults #> param([string]$Section) if (-not $script:IsLoaded) { Initialize-SimpleConfigurationManager } $defaultConfig = Get-DefaultConfiguration if ($Section) { if ($defaultConfig.ContainsKey($Section)) { $script:CurrentConfiguration[$Section] = $defaultConfig[$Section].Clone() Save-SimpleConfiguration } } else { $script:CurrentConfiguration = $defaultConfig Save-SimpleConfiguration } } function Export-SimpleConfiguration { <# .SYNOPSIS Export configuration as JSON #> $config = Get-SimpleConfiguration return $config | ConvertTo-Json -Depth 10 } function Get-SimpleConfigurationInfo { <# .SYNOPSIS Get configuration system information #> $defaultConfig = Get-DefaultConfiguration $hasCustomizations = $false if ($script:CurrentConfiguration) { # Simple comparison to detect customizations $currentJson = $script:CurrentConfiguration | ConvertTo-Json -Depth 10 $defaultJson = $defaultConfig | ConvertTo-Json -Depth 10 $hasCustomizations = $currentJson -ne $defaultJson } return @{ ConfigurationFile = $script:ConfigurationFile ConfigurationDirectory = $script:ConfigurationDirectory IsLoaded = $script:IsLoaded HasCustomizations = $hasCustomizations LastModified = if ($script:ConfigurationFile -and (Test-Path $script:ConfigurationFile)) { (Get-Item $script:ConfigurationFile).LastWriteTime } else { $null } } } function Get-ConfigurationSchema { <# .SYNOPSIS Get the configuration schema #> return $script:ConfigurationSchema } # Export functions Export-ModuleMember -Function @( 'Initialize-SimpleConfigurationManager', 'Get-SimpleConfiguration', 'Get-SimpleConfigurationSection', 'Get-SimpleConfigurationValue', 'Set-SimpleConfigurationValue', 'Reset-SimpleConfiguration', 'Export-SimpleConfiguration', 'Get-SimpleConfigurationInfo', 'Get-ConfigurationValidationErrors', 'Get-DefaultConfiguration', 'Get-ConfigurationSchema' ) |