Public/Set-ModuleConfig.ps1

<#
.SYNOPSIS
    Sets or updates configuration values for a module in the tcs suite.

.DESCRIPTION
    The Set-ModuleConfig function updates a module's settings file
    (<ApplicationData>/PowerShell/Config/<ModuleName>/Module.Config.json by default). Only the
    settings you pass are changed; other settings are kept. The file is created if it does not
    exist. The change also applies to the current session.

.PARAMETER ModuleName
    The name of the module to configure, for example 'tcs.core' or 'tcs.jira'. Only letters,
    digits, '.', '_' and '-' are allowed, and the name must start with a letter or digit.

.PARAMETER ModuleConfigFilePath
    The full path of a settings file to update, instead of resolving it from ModuleName.

.PARAMETER UpdateWarning
    Whether to show a warning when a newer version of the module is available.

.PARAMETER UpdateCheckIntervalHours
    How often (in hours) to check the PowerShell Gallery for a newer version. Default 24.

.PARAMETER Telemetry
    Whether anonymous usage telemetry is sent.

.PARAMETER TelemetryUri
    The HTTPS ingestion endpoint for telemetry. An empty string clears it.

.PARAMETER TelemetryApiKey
    The API key sent to the telemetry endpoint in the X-API-Key header. It is stored encrypted
    with Protect-ConfigValue (current user) and shown as '********' in -PassThru and
    Get-ModuleConfig output. An empty string clears it.

.PARAMETER Setting
    A hashtable of settings to change, for settings that have no parameter of their own (for
    example a module-specific setting from that module's Config/Module.Defaults.json). Each
    value is converted to the type of its default; a value that cannot be converted is
    rejected. A setting also passed as its own parameter (for example -Telemetry) uses the
    parameter value.

.PARAMETER Reset
    Restores the settings file to the defaults before applying any other settings passed. The
    defaults include the module's own Config/Module.Defaults.json when the module is loaded or
    installed.

.PARAMETER PassThru
    Outputs the resulting settings as a hashtable.

.INPUTS
    None
    This function does not accept pipeline input.

.OUTPUTS
    System.Collections.Hashtable
    When PassThru is specified. The telemetry API key is masked.

.EXAMPLE
    Set-ModuleConfig -ModuleName 'tcs.core' -UpdateWarning $false

    Turns off update warnings for tcs.core.

.EXAMPLE
    Set-ModuleConfig -ModuleName 'tcs.jira' -Telemetry $false

    Turns off telemetry for tcs.jira.

.EXAMPLE
    Set-ModuleConfig -ModuleName 'tcs.jira' -Setting @{ DefaultProject = 'OPS'; PageSize = 100 }

    Changes module-specific settings that have no parameter of their own.

.EXAMPLE
    Set-ModuleConfig -ModuleName 'tcs.core' -Reset -PassThru

    Restores the defaults and returns the resulting settings.

.NOTES
    Author: Nigel Tatschner
    Company: TheCodeSaiyan

.LINK
    Get-ModuleConfig
#>

function Set-ModuleConfig {
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'ByName')]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ByName', HelpMessage = 'Name of the module to configure.')]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern('^[A-Za-z0-9][A-Za-z0-9._-]*$')]
        [string]$ModuleName,

        [Parameter(Mandatory, ParameterSetName = 'ByPath', HelpMessage = 'Path of the module settings file.')]
        [ValidateNotNullOrEmpty()]
        [string]$ModuleConfigFilePath,

        [Parameter(HelpMessage = 'Show a warning when an update is available.')]
        [bool]$UpdateWarning,

        [Parameter(HelpMessage = 'Hours between update checks.')]
        [ValidateRange(1, 8760)]
        [int]$UpdateCheckIntervalHours,

        [Parameter(HelpMessage = 'Send anonymous usage telemetry.')]
        [bool]$Telemetry,

        [Parameter(HelpMessage = 'HTTPS telemetry ingestion endpoint.')]
        [AllowEmptyString()]
        [ValidateScript({ [string]::IsNullOrEmpty($_) -or $_ -match '^https://' })]
        [string]$TelemetryUri,

        [Parameter(HelpMessage = 'API key for the telemetry endpoint.')]
        [AllowEmptyString()]
        [string]$TelemetryApiKey,

        [Parameter(HelpMessage = 'Other settings to change, as a hashtable.')]
        [ValidateNotNull()]
        [hashtable]$Setting,

        [Parameter(HelpMessage = 'Restore the default settings first.')]
        [switch]$Reset,

        [Parameter(HelpMessage = 'Output the resulting settings.')]
        [switch]$PassThru
    )

    if ($PSCmdlet.ParameterSetName -eq 'ByName') {
        $ModuleConfigFilePath = Join-Path -Path (Join-Path -Path (Get-ModuleConfigRoot) -ChildPath $ModuleName) -ChildPath 'Module.Config.json'
    }
    else {
        $ModuleName = Split-Path -Path (Split-Path -Path $ModuleConfigFilePath -Parent) -Leaf
    }

    # Use the module's own defaults too, so -Reset matches what Get-ModuleConfig creates
    $modulePath = $null
    if (Test-ModuleNameValid -Name $ModuleName) {
        $modulePath = Resolve-ModuleBasePath -ModuleName $ModuleName
    }
    $defaults = Get-DefaultModuleConfig -ModulePath $modulePath

    $settings = @{}
    if ($Reset -or -not (Test-Path -LiteralPath $ModuleConfigFilePath)) {
        foreach ($key in $defaults.Keys) {
            $settings[$key] = $defaults[$key]
        }
    }
    else {
        $existing = Read-JsonFileAsHashtable -Path $ModuleConfigFilePath
        foreach ($key in $existing.Keys) {
            $settings[$key] = ConvertTo-ConfigValueType -Value $existing[$key] -DefaultValue $defaults[$key] -Key $key
        }
    }

    $changes = @{}
    if ($Setting) {
        foreach ($key in $Setting.Keys) {
            $name = [string]$key
            if ([string]::IsNullOrWhiteSpace($name) -or $name -in $script:ReservedConfigKeys) {
                throw "'$name' cannot be set; it describes the loaded module and is not stored."
            }
            $changes[$name] = ConvertTo-ConfigValueType -Value $Setting[$key] -DefaultValue $defaults[$name] -Key $name -Strict
        }
    }
    foreach ($name in @('UpdateWarning', 'UpdateCheckIntervalHours', 'Telemetry', 'TelemetryUri', 'TelemetryApiKey')) {
        if ($PSBoundParameters.ContainsKey($name)) {
            $changes[$name] = $PSBoundParameters[$name]
        }
    }
    foreach ($name in $changes.Keys) {
        $settings[$name] = $changes[$name]
    }

    if ($PSCmdlet.ShouldProcess($ModuleConfigFilePath, 'Update module settings')) {
        # Never store the telemetry API key in plain text (a plain key saved by 0.3.0 is protected now)
        $apiKey = [string]$settings['TelemetryApiKey']
        if (-not [string]::IsNullOrEmpty($apiKey) -and -not $apiKey.StartsWith('tcs:v1:')) {
            $settings['TelemetryApiKey'] = Protect-ConfigValue -Value $apiKey
        }
        Write-JsonFile -Path $ModuleConfigFilePath -Data $settings

        # Keep the current session in step with the file
        if ($script:ModuleConfigCache.ContainsKey($ModuleName)) {
            foreach ($key in $settings.Keys) {
                $script:ModuleConfigCache[$ModuleName][$key] = $settings[$key]
            }
        }
        $script:TelemetryConfigCache.Remove($ModuleName)
    }

    if ($PassThru) {
        Get-MaskedModuleConfig -Config $settings
    }
}