Modules/businessdev.ALbuild.Feeds/Private/ConvertTo-BcFeedDefinition.ps1

function ConvertTo-BcFeedDefinition {
    <#
    .SYNOPSIS
        Normalises an albuild.json feed entry into a feed definition (name, url, token, idScheme, kind).
 
    .DESCRIPTION
        Internal helper. A feed entry in albuild.json is either:
          - a plain URL string (everything else defaulted), or
          - an object: { url, name?, kind?, idScheme?, apiKey?, apiKeyVariable? }.
 
        Only 'url' is required. 'kind' and 'idScheme' are optional - the resolver auto-detects
        metapackage/runtime indirection from package metadata and locates packages by app id via the
        feed's search service, so they are needed only to override an edge case (a feed that serves
        both a regular and a runtime app for the same version, or a non-standard id scheme on a feed
        without a usable search service). A private feed's access key is given inline with 'apiKey'
        or, to keep it out of the file, by naming an environment variable with 'apiKeyVariable' (a
        '${env:NAME}' placeholder is also expanded in any field). The legacy 'token'/'tokenEnv' names
        are still accepted as aliases.
 
    .PARAMETER Entry
        The raw feed entry (string or object from ConvertFrom-Json).
 
    .OUTPUTS
        Hashtable with Name, Url, Token, IdScheme, Kind - or $null when the entry has no URL.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)] [AllowNull()] [object] $Entry
    )

    # Expands '${env:NAME}' placeholders against the current environment.
    $expandEnv = {
        param([string] $value)
        if ([string]::IsNullOrEmpty($value)) { return '' }
        return [regex]::Replace($value, '\$\{env:([^}]+)\}', {
                param($m) "$([Environment]::GetEnvironmentVariable($m.Groups[1].Value))" })
    }

    # Reads a field from a string/object/dictionary, case-insensitively, with a default.
    $field = {
        param([object] $source, [string] $name, [object] $default)
        if ($null -eq $source) { return $default }
        if ($source -is [System.Collections.IDictionary]) {
            foreach ($key in $source.Keys) { if ("$key" -ieq $name) { return $source[$key] } }
            return $default
        }
        $property = $source.PSObject.Properties | Where-Object { $_.Name -ieq $name } | Select-Object -First 1
        if ($property -and $null -ne $property.Value) { return $property.Value }
        return $default
    }

    # Derives a feed name from its URL: the Azure DevOps '_packaging/<name>/' segment, else the host.
    $nameFromUrl = {
        param([string] $url)
        $match = [regex]::Match($url, '_packaging/([^/]+)/')
        if ($match.Success) { return $match.Groups[1].Value }
        try { return ([uri] $url).Host } catch { return 'feed' }
    }

    if ($Entry -is [string]) {
        $url = & $expandEnv $Entry
        if (-not $url) { return $null }
        return @{ Name = (& $nameFromUrl $url); Url = $url; Token = ''; IdScheme = '{publisher}.{name}.{id}'; Kind = 'apps' }
    }

    $url = & $expandEnv "$(& $field $Entry 'url' '')"
    if (-not $url) {
        Write-ALbuildLog -Level Warning "Ignoring a feed entry in the project config that has no 'url'."
        return $null
    }

    # Access key for a private feed. Provide it inline with 'apiKey' (a ${env:NAME} placeholder is
    # still expanded), or name the environment variable that holds it with 'apiKeyVariable'. The
    # legacy 'token' / 'tokenEnv' names remain accepted as aliases. Inline wins over a variable.
    $token = & $expandEnv "$(& $field $Entry 'apiKey' (& $field $Entry 'token' ''))"
    if (-not $token) {
        $apiKeyVariable = "$(& $field $Entry 'apiKeyVariable' (& $field $Entry 'tokenEnv' ''))"
        if ($apiKeyVariable) { $token = "$([Environment]::GetEnvironmentVariable($apiKeyVariable))" }
    }

    $name = "$(& $field $Entry 'name' '')"
    if (-not $name) { $name = & $nameFromUrl $url }

    $idScheme = "$(& $field $Entry 'idScheme' '{publisher}.{name}.{id}')"

    $kind = "$(& $field $Entry 'kind' 'apps')".ToLowerInvariant()
    if ($kind -notin @('symbols', 'apps', 'runtime')) {
        Write-ALbuildLog -Level Warning "Feed '$name' has an unknown kind '$kind'; using 'apps'."
        $kind = 'apps'
    }

    return @{ Name = $name; Url = $url; Token = $token; IdScheme = $idScheme; Kind = $kind }
}