Public/Get-ModuleConfig.ps1

<#
.SYNOPSIS
    Retrieves the configuration for a PowerShell module in the tcs suite.

.DESCRIPTION
    The Get-ModuleConfig function finds the module that owns CommandPath (by walking up to the
    nearest module manifest) and returns its configuration as a hashtable.

    The configuration is built from:
      1. The tcs.core defaults (Config/Module.Defaults.json in tcs.core).
      2. The calling module's own Config/Module.Defaults.json, if it has one.
      3. The user's settings file:
         <ApplicationData>/PowerShell/Config/<ModuleName>/Module.Config.json
         (the TCS_CONFIG_ROOT environment variable overrides the root folder).

    The settings file is created with the defaults the first time a module is loaded so users
    can edit it. After that it is only read; change settings with Set-ModuleConfig.

    A value that cannot be read as the type of its default, or that is out of range (for
    example UpdateCheckIntervalHours below 1), is replaced by the default for that setting
    only; the other settings in the file are still used.

    The returned hashtable also contains ModuleName, ModulePath, ModuleVersion,
    ModuleConfigPath and ModuleConfigFilePath. The telemetry API key, if one is set, is shown
    as '********'.

.PARAMETER CommandPath
    The path of the calling script or module file, normally $PSCommandPath. When omitted, the
    path of the calling script is used.

.INPUTS
    None
    This function does not accept pipeline input.

.OUTPUTS
    System.Collections.Hashtable

.EXAMPLE
    $config = Get-ModuleConfig -CommandPath $PSCommandPath

    Retrieves the configuration for the module that contains the calling script.

.EXAMPLE
    if ((Get-ModuleConfig).UpdateWarning) { 'Update warnings are on' }

    Uses the calling script's path automatically.

.NOTES
    Author: Nigel Tatschner
    Company: TheCodeSaiyan

    If the settings file cannot be written (for example a read-only profile), defaults are
    used for the session and a verbose message is written.

.LINK
    Set-ModuleConfig
#>

function Get-ModuleConfig {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param (
        [Parameter(Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string]$CommandPath
    )

    if (-not $PSBoundParameters.ContainsKey('CommandPath')) {
        $CommandPath = (Get-PSCallStack)[1].ScriptName
        if ([string]::IsNullOrEmpty($CommandPath)) {
            throw 'CommandPath was not supplied and the caller has no script path. Pass -CommandPath $PSCommandPath.'
        }
    }
    Write-Verbose "CommandPath: $CommandPath"

    $manifest = Find-ModuleManifestFile -StartPath $CommandPath
    if (-not $manifest) {
        throw "No module manifest (.psd1) was found in or above '$CommandPath'."
    }

    $moduleName = $manifest.BaseName
    $modulePath = $manifest.DirectoryName
    $moduleVersion = (Import-PowerShellDataFile -LiteralPath $manifest.FullName).ModuleVersion
    $moduleConfigPath = Join-Path -Path (Get-ModuleConfigRoot) -ChildPath $moduleName
    $moduleConfigFilePath = Join-Path -Path $moduleConfigPath -ChildPath 'Module.Config.json'
    Write-Verbose "Module '$moduleName' $moduleVersion at '$modulePath'; config file '$moduleConfigFilePath'"

    $defaults = Get-DefaultModuleConfig -ModulePath $modulePath
    $config = @{}
    foreach ($key in $defaults.Keys) {
        $config[$key] = $defaults[$key]
    }

    if (Test-Path -LiteralPath $moduleConfigFilePath) {
        try {
            $userConfig = Read-JsonFileAsHashtable -Path $moduleConfigFilePath
            foreach ($key in $userConfig.Keys) {
                $config[$key] = ConvertTo-ConfigValueType -Value $userConfig[$key] -DefaultValue $defaults[$key] -Key $key
            }
        }
        catch {
            Write-Warning "The settings file '$moduleConfigFilePath' could not be read and defaults are being used. Fix or remove the file, or run 'Set-ModuleConfig -ModuleName $moduleName -Reset'. Error: $($_.Exception.Message)"
        }
    }
    else {
        try {
            Write-JsonFile -Path $moduleConfigFilePath -Data $defaults
            if ($config['Telemetry'] -eq $true -and $env:TCS_TELEMETRY_OPTOUT -notin @('1', 'true', 'yes')) {
                Write-Information -MessageData ("$moduleName collects anonymous usage telemetry (command name, duration, success and PowerShell/OS version; no user, machine or path details). " +
                    "Turn it off with 'Set-ModuleConfig -ModuleName $moduleName -Telemetry `$false' or by setting the TCS_TELEMETRY_OPTOUT environment variable to 1.") -InformationAction Continue
            }
        }
        catch {
            Write-Verbose "Could not create '$moduleConfigFilePath'; using defaults for this session. $($_.Exception.Message)"
        }
    }

    # Values derived from the loaded module always win over anything stored in the file
    $config['ModuleName'] = $moduleName
    $config['ModulePath'] = $modulePath
    $config['ModuleVersion'] = $moduleVersion
    $config['ModuleConfigPath'] = $moduleConfigPath
    $config['ModuleConfigFilePath'] = $moduleConfigFilePath

    $script:ModuleConfigCache[$moduleName] = $config
    # The session cache keeps the stored API key for telemetry; callers only see it masked
    return (Get-MaskedModuleConfig -Config $config)
}