Private/New-DscResourceAdapterScript.ps1

function New-DscResourceAdapterScript {
    <#
        .SYNOPSIS
            Generates the content of a resource.ps1 adapter script that
            bridges DSC v3 JSON-based invocation to PowerShell class-based
            DSC resources. Supports multiple resource classes.
 
            Wraps resource logic in a PowerShell runspace to capture PS
            streams (Error, Warning, Information, Verbose, Debug) and
            convert them to DSC-compatible JSON trace output on stderr.
 
        .DESCRIPTION
            PS stream → DSC trace mapping:
              Write-Error → error
              Write-Warning → warn
              Write-Information → info
              Write-Verbose → debug (must use -Verbose explicitly)
              Write-Debug → trace (must use -Debug explicitly)
 
            $env:DSC_TRACE_LEVEL controls which streams are captured.
            Error stream records cause a non-zero exit code.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [PSCustomObject[]]$ResourceInfos,

        [Parameter(Mandatory)]
        [string]$ModuleFileName
    )

    $isModule = $ModuleFileName -match '\.psm1$'

    $allMethods = foreach ($info in $ResourceInfos) {
        if ($info.Methods.Count -gt 0) { $info.Methods } else { @('Get','Set','Test') }
    }
    $validOps   = @($allMethods | Sort-Object -Unique | ForEach-Object { $_.ToLower() })
    $opsString  = "'" + ($validOps  -join "', '") + "'"

    $classNames   = @($ResourceInfos | ForEach-Object { $_.ClassName })
    $typesString  = "'" + ($classNames -join "', '") + "'"

    $switchLines = foreach ($name in $classNames) {
        if ($isModule) {
            " '$name' { GetTypeInstanceFromModule -ModulePath `$modulePath -ClassName '$name' }"
        } else {
            " '$name' { [$name]::new() }"
        }
    }
    $switchBlock = ($switchLines -join "`n") + "`n" +
                   " default { throw `"Unknown resource type: `$ResourceType. Valid: $($classNames -join ', ')`" }"

    # Build a map of visible (non-hidden) property names per resource type.
    # Get-DscResourceAstInfo already excludes hidden members, so
    # $info.Properties contains only the properties we want in output.
    $propertyMapLines = foreach ($info in $ResourceInfos) {
        $propNames = @($info.Properties | ForEach-Object { "'$($_.Name)'" }) -join ', '
        " '$($info.ClassName)' = @($propNames)"
    }
    $propertyMapBlock = " `$visibleProperties = @{`n" + ($propertyMapLines -join "`n") + "`n }"

    if ($isModule) {
        $importSection = @"
    `$modulePath = Join-Path `$ScriptRoot '$ModuleFileName'
    `$null = Import-Module `$modulePath -Force
 
    function GetTypeInstanceFromModule {
        param(
            [Parameter(Mandatory)]
            [string]`$ModulePath,
            [Parameter(Mandatory)]
            [string]`$ClassName
        )
        & (Import-Module `$ModulePath -PassThru) ([scriptblock]::Create("`[`$ClassName]::new()"))
    }
"@

    } else {
        $importSection = " . `"`$ScriptRoot/$ModuleFileName`""
    }

    $opCases = [System.Collections.Generic.List[string]]::new()

    if ($validOps -contains 'get') {
        $opCases.Add(@'
        'get' {
            $resource.Get() | Select-Object -Property $visibleProperties[$ResourceType]
        }
'@
)
    }

    if ($validOps -contains 'set') {
        $opCases.Add(@'
        'set' {
            $resource.Set()
        }
'@
)
    }

    if ($validOps -contains 'test') {
        $opCases.Add(@'
        'test' {
            $inDesiredState = $resource.Test()
            $state = $resource.Get() | Select-Object -Property $visibleProperties[$ResourceType] | ConvertTo-Json -Depth 10 | ConvertFrom-Json
            $state | Add-Member -NotePropertyName '_inDesiredState' -NotePropertyValue $inDesiredState -Force
            $state
        }
'@
)
    }

    if ($validOps -contains 'delete') {
        $opCases.Add(@'
        'delete' {
            $resource.Delete()
        }
'@
)
    }

    if ($validOps -contains 'export') {
        $opCases.Add(@'
        'export' {
            $resource.Export()
        }
'@
)
    }

    $opCasesBlock = $opCases -join "`n"

    $scriptTemplate = @'
# Generated by DscSchemaBuilder — DSC v3 resource adapter script
[CmdletBinding()]
param(
    [Parameter(ValueFromPipeline = $true)]
    [string]$InputJson,
 
    [Parameter()]
    [ValidateSet(%%OPS_STRING%%)]
    [string]$Operation,
 
    [Parameter(Mandatory)]
    [ValidateSet(%%TYPES_STRING%%)]
    [string]$ResourceType
)
 
$traceQueue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new()
$hadErrors = [System.Collections.Concurrent.ConcurrentQueue[bool]]::new()
 
function Write-DscTrace {
    param(
        [Parameter(Mandatory)]
        [ValidateSet('Error', 'Warn', 'Info', 'Debug', 'Trace')]
        [string]$Operation,
 
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]$Message,
 
        [switch]$Now
    )
 
    $trace = @{ $Operation.ToLower() = $Message }
 
    if ($Now) {
        $host.ui.WriteErrorLine(($trace | ConvertTo-Json -Compress))
    } else {
        $traceQueue.Enqueue($trace)
    }
}
 
trap {
    Write-DscTrace -Operation Error `
        -Message ($_ | Format-List -Force | Out-String) -Now
}
 
function Write-TraceQueue {
    $trace = $null
    while (-not $traceQueue.IsEmpty) {
        if ($traceQueue.TryDequeue([ref]$trace)) {
            $host.ui.WriteErrorLine(($trace | ConvertTo-Json -Compress))
        }
    }
}
 
$ps = [PowerShell]::Create().AddScript({
    [CmdletBinding()]
    param(
        [string]$InputJson,
        [string]$Operation,
        [string]$ResourceType,
        [string]$ScriptRoot
    )
 
    trap {
        Write-Error ($_ | Format-List -Force | Out-String)
    }
 
    $DebugPreference = 'SilentlyContinue'
    $VerbosePreference = 'SilentlyContinue'
    $ErrorActionPreference = 'Continue'
    $InformationPreference = 'Continue'
    $ProgressPreference = 'SilentlyContinue'
 
%%IMPORT_SECTION%%
 
    $resource = switch ($ResourceType) {
%%SWITCH_BLOCK%%
    }
 
%%PROPERTY_MAP%%
 
    $inputObject = $InputJson | ConvertFrom-Json
    foreach ($prop in $inputObject.PSObject.Properties) {
        if ($resource.PSObject.Properties.Name -contains $prop.Name) {
            $resource.($prop.Name) = $prop.Value
        }
    }
 
    switch ($Operation) {
%%OP_CASES_BLOCK%%
    }
}).AddParameter('InputJson', $InputJson
).AddParameter('Operation', $Operation
).AddParameter('ResourceType', $ResourceType
).AddParameter('ScriptRoot', $PSScriptRoot)
 
enum DscTraceLevel { Error; Warn; Info; Debug; Trace }
 
$traceLevel = if ($env:DSC_TRACE_LEVEL) {
    try { [DscTraceLevel]$env:DSC_TRACE_LEVEL }
    catch {
        Write-DscTrace -Operation Warn -Now `
            -Message "Invalid DSC_TRACE_LEVEL '$env:DSC_TRACE_LEVEL'. Defaulting to Warn."
        [DscTraceLevel]::Warn
    }
} else { [DscTraceLevel]::Warn }
 
$null = Register-ObjectEvent -InputObject $ps.Streams.Error `
    -EventName DataAdding `
    -MessageData @{ traceQueue = $traceQueue; hadErrors = $hadErrors } `
    -Action {
        $q = $Event.MessageData.traceQueue
        $q.Enqueue(@{ error = [string]$EventArgs.ItemAdded })
        $Event.MessageData.hadErrors.Enqueue($true)
    }
 
if ($traceLevel -ge [DscTraceLevel]::Warn) {
    $null = Register-ObjectEvent -InputObject $ps.Streams.Warning `
        -EventName DataAdding -MessageData $traceQueue -Action {
            $q = $Event.MessageData
            $q.Enqueue(@{ warn = $EventArgs.ItemAdded.Message })
        }
}
 
if ($traceLevel -ge [DscTraceLevel]::Info) {
    $null = Register-ObjectEvent -InputObject $ps.Streams.Information `
        -EventName DataAdding -MessageData $traceQueue -Action {
            $q = $Event.MessageData
            if ($null -ne $EventArgs.ItemAdded.MessageData) {
                $q.Enqueue(@{ info = $EventArgs.ItemAdded.MessageData.ToString() })
            }
        }
}
 
if ($traceLevel -ge [DscTraceLevel]::Debug) {
    $null = Register-ObjectEvent -InputObject $ps.Streams.Verbose `
        -EventName DataAdding -MessageData $traceQueue -Action {
            $q = $Event.MessageData
            $q.Enqueue(@{ debug = $EventArgs.ItemAdded.Message })
        }
}
 
if ($traceLevel -ge [DscTraceLevel]::Trace) {
    $null = Register-ObjectEvent -InputObject $ps.Streams.Debug `
        -EventName DataAdding -MessageData $traceQueue -Action {
            $q = $Event.MessageData
            $q.Enqueue(@{ trace = $EventArgs.ItemAdded.Message })
        }
}
 
$outputObjects = [System.Collections.Generic.List[object]]::new()
 
try {
    $asyncResult = $ps.BeginInvoke()
    while (-not $asyncResult.IsCompleted) {
        Write-TraceQueue
        Start-Sleep -Milliseconds 100
    }
    $outputCollection = $ps.EndInvoke($asyncResult)
    Write-TraceQueue
 
    foreach ($output in $outputCollection) {
        $outputObjects.Add($output)
    }
}
catch {
    Write-DscTrace -Now -Operation Error -Message $_
    exit 1
}
finally {
    $ps.Dispose()
    Get-EventSubscriber | Unregister-Event
}
 
Start-Sleep -Milliseconds 200
if ($hadErrors.Count -gt 0) {
    Write-DscTrace -Now -Operation Error `
        -Message 'Errors occurred during execution. Check previous traces.'
    exit 1
}
 
foreach ($obj in $outputObjects) {
    $obj | ConvertTo-Json -Depth 10 -Compress
}
'@


    $script = $scriptTemplate.
        Replace('%%OPS_STRING%%', $opsString).
        Replace('%%TYPES_STRING%%', $typesString).
        Replace('%%IMPORT_SECTION%%', $importSection).
        Replace('%%SWITCH_BLOCK%%', $switchBlock).
        Replace('%%PROPERTY_MAP%%', $propertyMapBlock).
        Replace('%%OP_CASES_BLOCK%%', $opCasesBlock)

    return $script
}