Modules/businessdev.ALbuild.Core/Classes/ALbuildConfig.ps1

# ALbuild's typed configuration objects. All three live in one file on purpose: PowerShell class
# inheritance must resolve the base type at parse time, which is only reliable when the base and
# its derived classes are defined together (dot-sourcing a derived class from a separate file does
# not see a base defined in another runtime-dot-sourced file).
#
# Derived classes declare their own strongly-typed settings (as real properties); all the name
# lookup / merge / serialisation behaviour lives in the base once, so we never reinvent a "config
# shape" as a loose PSCustomObject.
class ALbuildConfigBase {
    # Returns the names of all settings (the declared public properties of the concrete type).
    [string[]] Keys() {
        return ($this | Get-Member -MemberType Property | Select-Object -ExpandProperty Name)
    }

    [bool] Has([string] $name) {
        return ($this.Keys() -contains $name)
    }

    # Returns the value of a setting by name (case-insensitive), throwing for unknown names.
    [object] Get([string] $name) {
        $match = $this.Keys() | Where-Object { $_ -ieq $name } | Select-Object -First 1
        if (-not $match) {
            throw "Unknown ALbuild configuration setting '$name'. Known settings: $($this.Keys() -join ', ')."
        }
        return $this.$match
    }

    # Applies overrides from a hashtable (case-insensitive). Unknown keys are ignored with a
    # warning when $ignoreUnknown is $true (merging a config file), otherwise they throw.
    [void] Apply([hashtable] $overrides, [bool] $ignoreUnknown) {
        foreach ($key in $overrides.Keys) {
            $match = $this.Keys() | Where-Object { $_ -ieq [string]$key } | Select-Object -First 1
            if ($match) {
                $this.$match = $overrides[$key]
            }
            elseif (-not $ignoreUnknown) {
                throw "Unknown ALbuild configuration setting '$key'. Known settings: $($this.Keys() -join ', ')."
            }
            else {
                Write-Warning "ALbuild: ignoring unknown configuration setting '$key'."
            }
        }
    }

    # Serialises to an ordered hashtable for persistence.
    [System.Collections.Specialized.OrderedDictionary] ToOrderedDictionary() {
        $dict = [ordered]@{}
        foreach ($key in $this.Keys()) {
            $dict[$key] = $this.$key
        }
        return $dict
    }
}

# Machine/runtime settings for the ALbuild tooling itself. This is NOT a per-workspace, committed
# file: it lives under the user's application-data folder (see Get-ALbuildConfigPath) and holds
# only host concerns - cache locations, retry behaviour, telemetry and licensing. Project/build
# choices (country, artifact type, version, test runner, feeds) belong to ALbuildProjectConfig.
class ALbuildConfig : ALbuildConfigBase {
    # Cache locations
    [string]   $BaseFolder
    [string]   $ArtifactCacheFolder
    [string]   $PackageCacheFolder

    # Process / retry behaviour
    [int]      $ProcessRetryCount
    [int]      $ProcessRetryDelaySeconds

    # Telemetry (opt-in)
    [bool]     $TelemetryEnabled
    [string]   $TelemetryConnectionString

    # Licensing (free tier performs no check; licensed features verify against this service)
    [string]   $LicensingBaseUrl
    [string]   $LicenseAppId

    ALbuildConfig() {
        $base = Join-Path -Path ([System.Environment]::GetFolderPath('LocalApplicationData')) -ChildPath 'ALbuild'

        $this.BaseFolder                = $base
        $this.ArtifactCacheFolder       = Join-Path -Path $base -ChildPath 'artifacts'
        $this.PackageCacheFolder        = Join-Path -Path $base -ChildPath 'packages'
        $this.ProcessRetryCount         = 3
        $this.ProcessRetryDelaySeconds  = 5
        $this.TelemetryEnabled          = $false
        $this.TelemetryConnectionString = ''
        $this.LicensingBaseUrl          = 'https://license.365businessapi.com/api'
        $this.LicenseAppId              = '59261337-6a43-4884-98ab-fc62b6230547'
    }
}

# Developer-facing, committed project configuration. One file per workspace root and, optionally,
# one per app folder in a multi-root workspace (app + test). The app-folder file is merged on top
# of the workspace-root file, so an app can override the workspace defaults (e.g. a different
# country or a specific test runner). Loaded by Get-ALbuildProjectConfig from 'albuild.json'
# (canonical) or a deprecated 'pipeline.config' (V1 fallback).
class ALbuildProjectConfig : ALbuildConfigBase {
    # BC target selection (artifact resolution / container)
    [string]   $Country               # localisation of the BC target version (e.g. w1, de, fr)
    [string]   $ArtifactType          # Sandbox | OnPrem
    [string]   $BcVersion             # explicit version or prefix; empty = newest matching
    [string]   $Select                # Latest | First | Closest | NextMinor | NextMajor

    # Test execution
    [int]      $TestRunnerCodeunitId  # 0 = use the isolation default (130450/130451)
    [string]   $TestSuite
    [bool]     $DisableTestIsolation

    # Dependency feeds (in addition to any registered with Register-BcFeed). Each entry is either a
    # plain feed URL (string) or an object { url, name?, kind?, idScheme?, token?, tokenEnv? } - see
    # ConvertTo-BcFeedDefinition. Only 'url' is required; everything else is auto-detected.
    [object[]] $Feeds

    # Multi-root build: folder leaf names to exclude from the pipeline (e.g. 'migration'). The build
    # tasks merge this with the pipeline 'excludeProjects' parameter. See Get-BcProjectBuildOrder.
    [string[]] $ExcludeProjects

    # Dependency apps that are NOT resolved from a NuGet feed. Both are staged into each project's
    # .alpackages and pinned as a satisfied baseline for NuGet resolution (so they are not also
    # fetched from a feed), then installed into the build container with the feed-resolved apps.
    # UniversalPackages: Azure DevOps Universal feed packages, each an object
    # { feed, name, version?, organization?, project? } (organization defaults to the collection
    # URI). For shared symbol bundles / apps published to a Universal feed.
    # LocalPackages: repository folders holding committed .app files (for ISVs without a public
    # feed, e.g. CKL). Paths are relative to the repository root. Default: '.dependencies'.
    [object[]] $UniversalPackages
    [string[]] $LocalPackages

    ALbuildProjectConfig() {
        $this.Country              = 'w1'
        $this.ArtifactType         = 'Sandbox'
        $this.BcVersion            = ''
        $this.Select               = 'Latest'
        $this.TestRunnerCodeunitId = 0
        $this.TestSuite            = 'DEFAULT'
        $this.DisableTestIsolation = $false
        $this.Feeds                = @()
        $this.ExcludeProjects      = @()
        $this.UniversalPackages    = @()
        $this.LocalPackages        = @('.dependencies')
    }

    # The effective test-runner codeunit: an explicit id, otherwise the isolation default.
    [int] EffectiveTestRunnerCodeunitId() {
        if ($this.TestRunnerCodeunitId -gt 0) { return $this.TestRunnerCodeunitId }
        if ($this.DisableTestIsolation) { return 130451 }
        return 130450
    }
}