Public/Initialize-ExpressionCache.ps1

<#
.SYNOPSIS
Initializes ExpressionCache and registers cache providers.
 
.DESCRIPTION
Initialize-ExpressionCache sets up module-wide state (e.g., AppName used in key prefixes) and
registers one or more providers. Each provider is a hashtable with a 'Key' and a 'Config'.
 
❗ CONFIGURATION SEMANTICS
- Replacement, not merge: when you pass a Config for a built-in or previously-registered provider,
  the supplied object becomes the provider’s **entire** Config. Defaults/previous values are NOT merged.
  Any omitted settings will be unset (or fall back to the provider’s own internal defaults if it has them).
- Re-initializing: calling Initialize-ExpressionCache again with the same provider Key updates that
  provider using the same replacement semantics.
 
Tip: If you want to modify just one or two settings while keeping the rest of the defaults, copy the
current config first (see examples) and then change the keys you care about.
 
.PARAMETER AppName
Application name used in key prefixes/namespacing.
 
.PARAMETER Providers
One or more provider definitions of the form:
@{ Key = '<ProviderName>'; Config = @{ ... provider settings ... } }
The supplied Config **replaces** any existing/default config for that provider (no merge).
 
.OUTPUTS
The list of registered provider objects.
 
.EXAMPLE
# Initialize with defaults only (LocalFileSystemCache)
Initialize-ExpressionCache -AppName 'MyApp'
 
.EXAMPLE
# Replace LocalFileSystemCache config entirely (no merge)
Initialize-ExpressionCache -AppName 'MyApp' -Providers @(
  @{ Key='LocalFileSystemCache'; Config = @{
        Prefix = 'ExpressionCache:v1:MyApp'
        CacheFolder = "$env:TEMP\ExpressionCache"
     } }
)
 
.EXAMPLE
# Preserve defaults but tweak one setting: copy then modify
$prov = Get-ExpressionCacheProvider -Name 'LocalFileSystemCache'
$cfg = [pscustomobject]@{} # shallow clone of current config
$prov.Config.PSObject.Properties | ForEach-Object {
  Add-Member -InputObject $cfg -MemberType NoteProperty -Name $_.Name -Value $_.Value
}
$cfg.Prefix = 'ExpressionCache:v1:MyApp'
Initialize-ExpressionCache -AppName 'MyApp' -Providers @(@{ Key='LocalFileSystemCache'; Config = $cfg })
 
.EXAMPLE
# Add/replace a Redis provider config (password via env var)
Initialize-ExpressionCache -AppName 'MyApp' -Providers @(
  @{ Key='Redis'; Config = @{
        Host = '127.0.0.1'
        Port = 6379
        Database = 2
        Prefix = 'ExpressionCache:v1:MyApp'
        Password = $env:EXPRCACHE_REDIS_PASSWORD
     } }
)
 
.LINK
Get-ExpressionCache
Get-ExpressionCacheProvider
New-ExpressionCacheKey
#>

function Copy-PSCustomObject {
  param([Parameter(Mandatory)][pscustomobject]$InputObject)
  $copy = [pscustomobject]@{}
  foreach ($p in $InputObject.PSObject.Properties) {
    # shallow copy is fine for our flat configs
    Add-Member -InputObject $copy -NotePropertyName $p.Name -NotePropertyValue $p.Value
  }
  return $copy
}

function Initialize-ExpressionCache {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory)][string]$AppName,
    [object[]]$Providers
  )

  $script:Config = [pscustomobject]@{
    AppName = $AppName
    Version = $Script:moduleData.ModuleVersion
  }
  $script:RegisteredStorageProviders = @()

  $defaults = Get-DefaultProviders

  # Treat -Providers as an allow-list when present; otherwise use defaults
  $explicit = $PSBoundParameters.ContainsKey('Providers') -and $Providers -and $Providers.Count -gt 0
  $selected = @()

  if (-not $explicit) {
    $selected = @($defaults.LocalFileSystemCache, $defaults.Redis)
  } else {
    foreach ($hint in ($Providers | Where-Object { $_ })) {
      $resolved = Resolve-Provider -Hint $hint -DefaultMap $defaults
      if (-not $resolved) { continue }

      # If matches a known default, overlay config onto a *clone* of the default
      $defaultMatch = switch -Regex ($resolved.Name) {
        '^LocalFileSystemCache$' { $defaults.LocalFileSystemCache; break }
        '^Redis$'                { $defaults.Redis; break }
      }

      if ($defaultMatch) {
        $mergedConfig = Copy-PSCustomObject $defaultMatch.Config
        if ($resolved.PSObject.Properties['Config'] -and $resolved.Config -is [System.Collections.IDictionary]) {
          Merge-ExpressionCacheConfig -Base $mergedConfig -Overrides $resolved.Config | Out-Null
        }

        # Build a merged spec keeping default hooks
        $selected += [pscustomobject]@{
          Name        = $defaultMatch.Name
          GetOrCreate = $defaultMatch.GetOrCreate
          Initialize  = $defaultMatch.Initialize
          ClearCache  = $defaultMatch.ClearCache
          Config      = $mergedConfig
        }
      }
      else {
        # Custom provider: use as-is (caller must supply functions & Config)
        $selected += $resolved
      }
    }
  }

  foreach ($p in $selected) {
    Add-ExpressionCacheProvider -Provider $p | Out-Null
  }

  return $script:RegisteredStorageProviders
}