Public/Get-HDTApplication.ps1

function Get-HDTApplication {
    <#
        .SYNOPSIS
            Reads an application, or the whole application catalog, out of the
            workspace.
 
        .DESCRIPTION
            THE CATALOG IS THE DIRECTORY. DESIGN 2.1 gives every application a
            folder under Applications\ holding its app.yaml and its source\
            payload, so there is no single catalog file to parse - reading one
            application is a read of one app.yaml, and reading the catalog is an
            enumeration of that folder. Two administrators adding applications
            never collide in one file, and a folder copy moves an application
            whole.
 
            It reads through an injected IFileSystem - never Get-Content - so the
            whole authoring path is provable under Pester with no share and no
            disk.
 
            Four steps per application, and a failure at any of them is a
            terminating HDTConfigurationError naming the file:
 
              1. read through IFileSystem;
              2. parse with ConvertFrom-HDTYaml, which turns a parser exception
                 into an error naming the file and the LINE;
              3. validate with Assert-HDTApplicationDocument, which names the file
                 and the offending key;
              4. project with ConvertTo-HDTApplicationCatalog, which resolves
                 SourcePath and applies every default.
 
            A FOLDER WITH NO app.yaml IS NOT AN APPLICATION, and enumeration skips
            it. A share people actually use collects stray folders, and a console
            that threw over one would be unusable. A folder that DOES hold an
            app.yaml and gets it wrong is a different thing entirely, and that
            fails naming the file.
 
            A WORKSPACE WITH NO Applications\ FOLDER RETURNS NOTHING rather than
            failing. Applications are optional; a workspace that deploys an image
            and no software is a legitimate workspace.
 
        .PARAMETER WorkspaceRoot
            The workspace root - a local path or a UNC share.
 
        .PARAMETER Id
            The catalog id, which is the folder name under Applications\. Omit it
            to read every application in the workspace.
 
        .PARAMETER FileSystem
            An IFileSystem - the real adapter in production,
            New-HDTFakeFileSystem in a test.
 
        .PARAMETER Content
            An IContentProvider, or nothing. When supplied, SourcePath is what it
            answered for Applications\<id>\source.
 
        .INPUTS
            None. This command does not accept pipeline input.
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject:
 
              SchemaVersion, Id, Name, Description, Install, Uninstall,
              SuccessCodes, RebootCodes, Detect, Dependencies, RunIn, Path,
              AppFolder and SourcePath.
 
            Detect is $null for an application that declares no detection rule,
            which is DESIGN 8's "install every time".
 
        .EXAMPLE
            Get-HDTApplication -WorkspaceRoot 'X:\Deploy' -Id '7Zip-24.09' -FileSystem (New-HDTFileSystem)
 
        .EXAMPLE
            Get-HDTApplication -WorkspaceRoot 'X:\Deploy' -FileSystem (New-HDTFileSystem) |
                Where-Object { $_.Dependencies.Count -gt 0 }
 
            The whole catalog, which is what Resolve-HDTApplicationOrder is handed.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $WorkspaceRoot,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string] $Id,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [object] $FileSystem,

        [Parameter()]
        [AllowNull()]
        [object] $Content = $null
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    $readOne = {
        param([string] $ApplicationId)

        $appFolder = Get-HDTWorkspacePath -Root $WorkspaceRoot -Kind Applications -ChildPath $ApplicationId
        $catalogPath = Get-HDTWorkspacePath -Root $WorkspaceRoot -Kind Applications -ChildPath $ApplicationId, 'app.yaml'

        $text = $FileSystem.ReadAllText($catalogPath)

        $document = ConvertFrom-HDTYaml -Yaml $text -Path $catalogPath
        Assert-HDTApplicationDocument -Document $document -Path $catalogPath

        try {
            return (ConvertTo-HDTApplicationCatalog -Document $document -AppFolder $appFolder `
                    -CatalogPath $catalogPath -Content $Content)
        } catch {
            # A provider refusal - a path that escapes the content root - is
            # reported naming app.yaml rather than surfacing as a
            # MethodInvocationException from a ScriptMethod three frames down.
            $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $catalogPath `
                        -Message ("the application source path could not be resolved: {0}" -f [string] $_.Exception.Message)))
        }
    }

    if ($PSBoundParameters.ContainsKey('Id')) {
        $catalogPath = Get-HDTWorkspacePath -Root $WorkspaceRoot -Kind Applications -ChildPath $Id, 'app.yaml'

        if (-not $FileSystem.TestPath($catalogPath)) {
            $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $catalogPath `
                        -Message ("no application with the id '{0}' is in this workspace. An application is a folder under Applications\ holding an app.yaml." -f $Id) `
                        -Category ObjectNotFound))
        }

        return (& $readOne $Id)
    }

    $applicationRoot = Get-HDTWorkspacePath -Root $WorkspaceRoot -Kind Applications

    if (-not $FileSystem.TestPath($applicationRoot)) { return }

    foreach ($child in @($FileSystem.GetChildItem($applicationRoot))) {
        $childId = [System.IO.Path]::GetFileName([string] $child)
        $catalogPath = Get-HDTWorkspacePath -Root $WorkspaceRoot -Kind Applications -ChildPath $childId, 'app.yaml'

        if (-not $FileSystem.TestPath($catalogPath)) { continue }

        & $readOne $childId
    }
}