Write-DSCResource.ps1

function Write-DSCResource
{
    <#
    .Synopsis
        Automatically generates the tedious code required to implement a DSC resource
    .Description
        Automatically generates the source code for a DSC resource.
 
        The DSC resource can be generated with a simple hashtable and a set of scriptblocks, or it can be implemented by wrapping one or more commands.
    .Example
        Write-DSCResource -Property @{
            OutputPath = "The output path"
            Base64Content = "The file contents, in base64"
        } -Test {
            if (-not [IO.File]::Exists($OutputPath)) {
                return $false
            }
            $b64 = [Convert]::FromBase64String($Base64Content)
            $md5 = [Security.Cryptography.MD5]::Create()
            $hash1 = [BitConverter]::ToString($md5.ComputeHash($b64))
            $hash2 = [BitConverter]::ToString($md5.ComputeHash([IO.File]::ReadAllBytes($OutputPath)))
            $hash1 -eq $hash2
        } -Get {
            @{
                OutputPath = $outputPath
                Base64Content = try {
                    [Convert]::ToBase64String([IO.File]::ReadAllBytes($outputPath))
                } catch {
                    $null
                }
            }
        } -Set {
            if (-not (Test-Path $outputPath)) {
                $nf = New-Item -Path $outputPath -Force -ItemType File
            }
            [IO.File]::WriteAllBytes($outputPath, [Convert]::FromBase64String($base64Content))
        } -KeyProperty OutputPath
         
    .Link
        Initialize-DSCResource
    .Link
        Reset-DSCResource
    #>

    [CmdletBinding(DefaultParameterSetName='PropertyHashtable')]
    [OutputType([Nullable])]
    param(
    # The name of the resource
    [Parameter(Mandatory=$true,Position=0)]
    [string]
    $ResourceName,

    # The root directory of the module in which the DSC resource will be contained.
    [Parameter(Position=1)]
    [string]
    $ModuleRoot,
    
    # A hashtable describing the properties of the DSC resource. The keys are the names of the properties, and the values are a description, with an optional type
    [Parameter(Mandatory=$true, ParameterSetName='PropertyHashtable', Position=3)]
    [ValidateScript({
        foreach ($kv in $_.GetEnumerator()) {
            if ($kv.Key -isnot [string]) {
                throw "$($kv.Key) must be a string"
            }
            if ($kv.Value -isnot [string]){
                if ($kv.Value -isnot [Object[]]) {
                    throw "$($kv.Key) must either be a string, a string and a type, or a string, a type, and a hashtable of information"
                }
                $description, $type, $RestOfInfo = $kv.Value 
                if ($description -isnot [string]) {
                    throw "$($kv.Key) must either be a string, a string and a type, or a string, a type, and a hashtable of information"
                }
                if (-not ($type -as [type])) {
                    throw "$($kv.Key) must either be a string, a string and a type, or a string, a type, and a hashtable of information"
                }
                
                if ($restOfInfo -and (-not ($RestOfInfo -as [Hashtable]))) {
                    throw "$($kv.Key) must either be a string, a string and a type, or a string, a type, and a hashtable of information"
                }
            }
        }
        return $true
    })]
    [Hashtable]
    $Property,

    # The names of the key properties.
    [Parameter(Mandatory=$true,ParameterSetName='PropertyHashtable',Position=2)]
    [Parameter(Mandatory=$true,ParameterSetName='WrapCommand',Position=2)]
    [string[]]
    $KeyProperty,

    # A list of read only properties
    [string[]]
    $ReadOnlyProperty,

    # A List of mandatory properties. These properties, while not used as a key, will be required for the get/test/set functions in the DSC resource.
    [Alias('RequiredProperty', 'MandatoryProperties', 'RequiredProperties')]
    [string[]]
    $MandatoryProperty,

    # A hashtable containing the names of properties and their potential values
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [ValidateScript({
        foreach ($kv in $_.GetEnumerator()) {
            if (-not ($kv.Value -as [string[]]) -and 
                -not ($kv.Value -as [Hashtable])) {
                throw "$($kv.Key) must be a list of valid values or a Hashtable"    
            }
        }
        return $true
    })]
    [Hashtable]
    $PropertyValueMap = @{},

    # A hashtable of containing valid values or enumerations for a particular property. The key is the name of the property. If the value is a list, the list will be the acceptable values for that property. If the value is a hashtable, the keys will be friendly values, and the values will be the underlying system value.
    [Parameter(ValueFromPipelineByPropertyName=$true)]    
    [Hashtable]
    $ValueMap,
        
    # The underlying implementation of Get-TargetResource. By default, this returns an empty hashtable.
    [ScriptBlock]
    $Get = {@{}},
    
    # The underlying implementation of Set-TargetResource. By default, this does nothing.
    [ScriptBlock]
    $Set = {},

    # The underlying implementation of Test-TargetResource. By default, this returns $false.
    [ScriptBlock]
    $Test = {return $false},
    
    # The version of the DSC resource. By default, 1.0.
    [Version]
    $Version = '1.0',

    # The Command that will be called when the resource is set. The Set Command is the only command required to wrap a DSC resource. The Test Command must use the same parameter names
    [Parameter(Mandatory=$true,ParameterSetName='WrapCommand',ValueFromPipeline=$true)]
    [Management.Automation.CommandInfo]
    $SetCommand,

    # The Command that will be called when the resource is requested. Output from this command will be converted into PowerShell hashtables.
    [Parameter(ParameterSetName='WrapCommand')]
    [Management.Automation.CommandInfo]
    $GetCommand,

    # The Test Command. A non-null or empty output from this command will be considered a pass
    [Parameter(ParameterSetName='WrapCommand')]
    [Management.Automation.CommandInfo]
    $TestCommand,

    # Default parameters that will be passed to the set command
    [Parameter(ParameterSetName='WrapCommand')]
    [Hashtable]
    $SetDefaultParameter = @{},

    # Default parameters that will be passed to the get command.
    [Parameter(ParameterSetName='WrapCommand')]
    [Hashtable]
    $GetDefaultParameter = @{},

    # Default parameters that will be passed to the test command.
    [Parameter(ParameterSetName='WrapCommand')]
    [Hashtable]
    $TestDefaultParameter = @{},
    
    # A list of parameters to be excluded from the Set command.
    [Parameter(ParameterSetName='WrapCommand')]
    [string[]]
    $SetParameterBlacklist,

    # A list of parameters to be excluded from the Test command. If the parameter is not in the Set blacklist, it will exist as a DSC resource setting but will be ignored when the underlying Test command is called.
    [Parameter(ParameterSetName='WrapCommand')]
    [string[]]
    $TestParameterBlackList,

    # A list of parameters to be excluded from the Get command. If the parameter is not in the Set blacklist, it will exist as a DSC resource setting but will be ignored when the underlying Get command is called.
    [Parameter(ParameterSetName='WrapCommand')]
    [string[]]
    $GetParameterBlackList,

    # A map of DSC resource setting names to the underlying parameters in the Set function
    [Parameter(ParameterSetName='WrapCommand')]
    [Hashtable]
    $SetParameterNameMap = @{},

    # A ScriptBlock used to interpret the results of the test command. The variable $testResult will contain the results of the test command. If no script block is provided, any output from the Test command will be converted to a boolean. No output, or an explicit output of false, will fail the test.
    [Parameter(ParameterSetName='WrapCommand')]
    [ScriptBlock]
    $ProcessTestCommandResult)

    begin {
        

        $myCmd = $MyInvocation.MyCommand
        #region Type Lookup Tables
        $CimTypeMap = @{
            [SByte]="Sint8"
            [String]="String"
            [Byte[]]="Uint8[]"
            [Single]="Real32"
            [Int32]="Sint32"
            [PSCredential]="MSFT_Credential"
            [PSCredential[]]="MSFT_Credential[]"
            [UInt64[]]="Uint64[]"
            [UInt64]="Uint64"
            [Char]="Char16"
            [Hashtable] = "Microsoft.Management.Infrastructure.CimInstance[]"
            [String[]]="String[]"
            [Double]="Real64"
            [Double[]]="Real64[]"
            [DateTime]="DateTime"
            [Byte]="Uint8"
            [Char[]]="Char16[]"
            [Int32[]]="Sint32[]"
            [Boolean]="Boolean"
            [Uint32]="Uint32"
            [Uint32[]]="Uint32[]"
            [Int16[]]="Sint16[]"
            [Single[]]="Real32[]"
            [UInt16]="Uint16"
            [Int64[]]="Sint64[]"
            [UInt16[]]="Uint16[]"
            [Int64]="Sint64"
            [SByte[]]="Sint8[]"
            [Boolean[]]="Boolean[]"
            [Int16]="Sint16"
            [DateTime[]]="DateTime[]"
            [Timespan] = "String"
            [Switch] = "Boolean"
            [ScriptBlock] = "String"
            [ScriptBlock[]] = "String[]"
            [TimeSpan[]] = "String[]"
        }
        
        $EmbeddedInstances = @{
            [Hashtable]    = "MSFT_KeyValuePair"
            [PSCredential] = "MSFT_Credential"

            [Hashtable[]]    = "MSFT_KeyValuePair"
            [PSCredential[]] = "MSFT_Credential"
            
        }
        #endregion Type Lookup Tables
    }

    process {        
        if ($PSCmdlet.ParameterSetName -eq 'WrapCommand') {
            $Splat = @{
                ResourceName=$ResourceName
                ModuleRoot=$ModuleRoot
                KeyProperty = $KeyProperty
                Version = $Version
            }

            $boundParams = @{} + $PSBoundParameters

            $paramNames = @{}
            $paramTypes = @{}

            
            $setCmdHelp = $setCommand | Get-Help
            foreach ($param in $setCmdHelp.Parameters.parameter) {
                if ($SetParameterBlacklist -contains $param.Name) {
                    Write-Verbose "Skipping $($param.name) because it was blacklisted"
                    continue
                }                

                if (-not $Splat.Property) {
                    $splat.Property = @{}
                }

                $parameterType = ($param.type.name -as [type])

                if (-not $parameterType -and $param.type.name -eq 'SwitchParameter') {
                    $parameterType = [switch]
                }
                if ($CimTypeMap.Keys -notcontains $parameterType) {
                    Write-Verbose "Skipping $($param.name) because $parameterType is not supported by CIM"
                    continue
                }

                $parameterName = 
                    if ($SetParameterNameMap.ContainsKey($param.Name)) {
                        $SetParameterNameMap[$param.Name]
                    } else {
                        $param.Name 
                    }
                if (-not $parameterName) { continue } 
                $splat.Property.($Param.Name) = @()
                $splat.Property.($Param.Name) += @($param.description | Select-Object -ExpandProperty Text) -join ([Environment]::NewLine)
                $splat.Property.($Param.Name) += $parameterType                                        
                $paramNames += @{$parameterName = $param.Name}
                $paramTypes += @{$parameterName = $parameterType }

                
                
                
                    
            }

            $null = $null

            $setScript = @"
`$splat = @{}
`$setCommand = `$executionContext.SessionState.InvokeCommand.GetCommand('$SetCommand', 'all')
$(@(foreach ($kv in $paramNames.GetEnumerator()) {
$inlineValue =
if ($setcommand.Parameters[$kv.Value].ParameterType -eq [ScriptBlock]) {
    "[ScriptBlock]::Create(`$$($kv.Key))"
} elseif ($setcommand.Parameters[$kv.Value].ParameterType -eq [ScriptBlock[]]) {
    "foreach (`$sb in `$$($kv.Key)) { [ScriptBlock]::Create(`$sb) } "
} else {
    "`$$($kv.Key)"
}
$null = $null
"if (`$psBoundParameters.ContainsKey('$($kv.Key)')) {
    `$splat['$($kv.Value)'] = $inlineValue
}"}) -join ([Environment]::NewLine))
$(
if ($SetParameterBlacklist) {
    @(foreach ($p in $SetParameterBlacklist) {
"`$splat.Remove('$p')"
    }) -join ([Environment]::NewLine)
})
$(@(foreach ($default in $SetDefaultParameter.GetEnumerator()) {
"if (-not `$splat.ContainsKey('$($default.Key)')) {
`$splat.$($default.Key) = $(
    if ($default.Value -is [string]) {
        "'$($default.Value)'"
    } elseif ($default.Value -is [Bool]) {
        "`$$($default.Value)"
    } else {
        "$($default.Value)"
    })
}"}) -join ([Environment]::NewLine))
$SetCommand @splat | Out-Null
"@

                
            

                
                
            $setscriptBlock = [ScriptBlock]::Create($setScript)

            if ("$Set") {
                $Splat.Set = $Set
            } elseif ($setscriptBlock) {
                $splat.Set = $setscriptBlock
            }
            

            if ("$get") {
                $Splat.Get = $Get
            } elseif ($GetCommand) {
                $GetScript = @"
`$splat = @{}
$(@(foreach ($kv in $paramNames.GetEnumerator()) {
"if (`$psBoundParameters.ContainsKey('$($kv.Key)')) {
    `$splat['$($kv.Value)'] = $(if ($setcommand.Parameters[$kv.Value].ParameterType -like "*Automation.ScriptBlock*") {"[ScriptBlock]::Create(`$$($kv.Key))"} else {"`$$($kv.Key)" })
}"}) -join ([Environment]::NewLine))
$(
foreach ($p in $getParameterBlackList) {
"`$splat.Remove('$p')
"
})
$(foreach ($default in $GetDefaultParameter.GetEnumerator()) {
"
if (-not `$splat.ContainsKey('$($default.Key)')) {
`$splat.$($default.Key) = $(
    if ($default.Value -is [string]) {
        "'$($default.Value)'"
    } elseif ($default.Value -is [Bool]) {
        "`$$($default.Value)"
    } else {
        "$($default.Value)"
    })"
})
 
`$getResults = $GetCommand @splat
`$getResultsHT = @{}
if (`$getResults -is [Hashtable]) {
    `$getResultsHT = `$getResults
} elseif (`$getResults) {
    foreach (`$prop in `$getResults.psobject.properties) {
        `$getResultsHT[`$prop.Name] = `$prop.Value
    }
}
return `$getResultsHT
"@


                $GetScriptBlock = [ScriptBlock]::Create($GetScript)

                if ("$Get") {
                    $splat.Get = $get
                } elseif ($GetScriptBlock) {
                    $Splat.Get = $GetScriptBlock
                }
            }
            
            if ($boundParams.Test) {
                $splat.Test = $test
            } elseif ($TestCommand) {
                $TestScript = @"
`$splat = @{}
$(@(foreach ($kv in $paramNames.GetEnumerator()) {
"if (`$psBoundParameters.ContainsKey('$($kv.Key)')) {
    `$splat['$($kv.Value)'] = $(if ($setcommand.Parameters[$kv.Value].ParameterType -like "*Automation.ScriptBlock*") {"[ScriptBlock]::Create(`$$($kv.Key))"} else {"`$$($kv.Key)" })
}"}) -join ([Environment]::NewLine))
$(
foreach ($p in $TestParameterBlacklist) {
"
`$splat.Remove('$p')
"
})
$(foreach ($default in $TestDefaultParameter.GetEnumerator()) {
"
if (-not `$splat.ContainsKey('$($default.Key)')) {
`$splat.$($default.Key) = $(
    if ($default.Value -is [string]) {
        "'$($default.Value)'"
    } elseif ($default.Value -is [Bool]) {
        "`$$($default.Value)"
    } else {
        "$($default.Value)"
    })"
})
`$TestResults = `$testResult = $TestCommand @splat
"@


if ($ProcessTestCommandResult) {
    $TestScript += $ProcessTestCommandResult
} else {
    $TestScript += @"
 
`$(`$testResults) -as [bool]
"@

}

                $TestScriptBlock = [ScriptBlock]::Create($TestScript)

                if ($TestScriptBlock) {
                    $Splat.Test = $TestScriptBlock
                }

            }

            Write-DSCResource @splat 
            return    
        }
        $schemaProperties = New-Object Collections.ArrayList
        

        $getParams = New-Object Collections.ArrayList
        $testParams = New-Object Collections.ArrayList
        $setParams = New-Object Collections.ArrayList
        

        # Go thru each property in the hashtable
        foreach ($kv in $Property.GetEnumerator()) {
            $attributeSection = New-Object Collections.ArrayList

            $propertyName = $kv.Key
            $PropertyDescription, $propertyType, $propertyRest  = @($kv.Value)

            if (-not $PropertyType) {
                $PropertyType = [string]
            }

            if (-not $propertyRest) {
                $propertyRest = @{} 
            } elseif ($propertyRest -as [Hashtable]) {
                foreach ($pr in ($propertyRest -as [Hashtable]).GetEnumerator()) {
                    $matchingParam = $null 
                    
                    if ($myCmd.Parameters.ContainsKey($pr.Key)) {
                        $matchingParam = $myCmd.Parameters[$pr.Key]
                    } else {
                        foreach ($p in $myCmd.Parameters) {
                            if ($p.Aliases -contains $pr.Key) {
                                $matchingParam = $p
                                break 
                            }
                        }
                    }

                    if ($matchingParam) {
                        
                        $existingVar = $ExecutionContext.SessionState.PSVariable.Get($pr.Key)
                        if ($existingVar -and $existingVar.Value -and $existingVar.Value.GetType().IsArray) {
                            $existingVar += $pr.Value 
                        } elseif (-not $existingVar) {
                            $ExecutionContext.SessionState.PSVariable.Set($pr.Key, $pr.Value) 
                        }                    
                    } elseif ('ValidValue', 'ValidValues','Value', 'Values', 'ValueMap' -contains 
                        $pr.Key) {
                        $PropertyValueMap.$propertyname = $pr.Value
                    }   
                }
            }



            if ($KeyProperty -contains $propertyName) {
                $null = $attributeSection.Add('Key')
            }

            if ($ReadOnlyProperty -contains $propertyName) {
                $null = $attributeSection.Add('Read')
            } elseif (-not ($KeyProperty -contains $propertyName)) {
                $null = $attributeSection.Add('Write')
            } 

            $isMandatory = 
                $KeyProperty -contains $propertyName -or 
                $MandatoryProperty -contains $propertyName


            

            if ($PropertyDescription)  {
                $null = $attributeSection.Add("Description(`"$($PropertyDescription.Replace('\','\\').Replace('"', '\"'))`")")
            }

            if ($PropertyValueMap.ContainsKey($propertyName)) {
                if ($PropertyValueMap[$propertyName] -is [Hashtable]) {
                    $valueKeys = New-Object Collections.ArrayList
                    $valueValues = New-Object Collections.ArrayList
                    foreach ($kv in $PropertyValueMap[$propertyName].GetEnumerator()) {
                        $null = $valueKeys.Add($kv.Key)
                        $null = $valueValues.Add($kv.Value)
                    }
                    $null = $attributeSection.Add("ValueMap{`"$($valueKeys -join '","')`"}")
                    $null = $attributeSection.Add("Values{`"$($valueValues -join '","')`"}")
                } else {
                    $null = $attributeSection.Add("ValueMap{`"$($PropertyValueMap[$propertyName] -join '","')`"}")
                    $null = $attributeSection.Add("Values{`"$($PropertyValueMap[$propertyName] -join '","')`"}")
                }                
            }


            $propertyPowerShellType = if ($PropertyType -eq [Hashtable]) {
                'Microsoft.Management.Infrastructure.CimInstance[]'
            } elseif ($propertyType -eq [ScriptBlock]) {
                'string'
            } elseif ($propertyType -eq [ScriptBlock[]]) {             
                'string[]'
            } elseif ($propertyType -eq [TimeSpan]) {             
                'string'
            } elseif ($propertyType -eq [TimeSpan[]]) {             
                'string[]'
            } else {
                $PropertyType
            }


            $paramStr = @"
[Parameter($(if ($isMandatory) { 'Mandatory=$true'}))]
[$propertyPowerShellType]
`$$PropertyName
"@


            if ($isMandatory) {
                $null = $getParams.Add($paramStr)
            }

            if ($ReadOnlyProperty -notcontains $PropertyName) {
                $null = $setParams.Add($paramStr)
                $null = $testParams.Add($paramStr)
            }


            $cimTypeString = 
                if ($PropertyType -and $CimTypeMap[$PropertyType]) {                    
                    if ($EmbeddedInstances.Contains($PropertyType)) {
                        $null = $attributeSection.Add("EmbeddedInstance(`"$($EmbeddedInstances[$PropertyType])`")")
                        'String'
                    } else {
                        $CimTypeMap[$PropertyType]
                    }
                } else {
                    'String'
                }

                $ArrayMarker = ''
                if ($acc.PropertyType.IsArray -or $PropertyType -eq [Hashtable]) {
                    $ArrayMarker = '[]'
                }

                if ($cimTypeString.EndsWith('[]')) {
                    $cimTypeString = $cimTypeString.TrimEnd('[]')
                    $ArrayMarker = '[]'
                }

            $null = $schemaProperties.Add("`t$(if ($attributeSection.Count -ge 1 ) { "[$($attributeSection -join ', ')]" }) $CimTypeString $($PropertyName)$ArrayMarker;")
        }



        $DscGet = @"
function Get-TargetResource {
    [CmdletBinding()]
    [OutputType([Hashtable])]
    param($($getParams -join ',
    '))
 
    $Get
}
"@


$DscSet = @"
function Set-TargetResource {
    [CmdletBinding()]
     
    param($($setParams -join ',
    '))
 
    $Set
}
"@


$dscTest = @"
function Test-TargetResource {
    [CmdletBinding()]
    [OutputType([bool])]
    param($($testParams -join ',
    '))
 
    $test
}
"@


$psm1 = "
$DscGet
 
$DscSet
 
$dscTest
 
Export-ModuleMember -Function Get-TargetResource, Set-TargetResource, Test-TargetResource
"



        if (-not $FriendlyName) { 
            $FriendlyName = $ResourceName
        }
        $versionString = "$($version.Major).$($version.Minor).$(if ($version.Build -gt 0) { $version.Build} else {0 }).$(if ($version.Revision -gt 0) {$version.Revision} else {0 } )"
        $resourceMof = @"
[ClassVersion("$VersionString"), FriendlyName("$FriendlyName")]
class $ResourceName : OMI_BaseResource
{
$($schemaProperties -join ([Environment]::NewLine))
};
"@

        
        if ($ModuleRoot) {
            $resourceDir = Join-Path $moduleRoot "DSCResources"

            $resourceDir = Join-Path $resourceDir $ResourceName
        
            if (-not (Test-Path $resourceDir)) {
                $ni = New-Item -ItemType 'Directory' -Path $resourceDir -Force 
                if (-not $ni ) { return } 
            }

            $psm1 | 
                Set-Content -Path (Join-Path $resourceDir "$ResourceName.psm1")

            $resourceMof | 
                Set-Content -Path (Join-Path $resourceDir "$ResourceName.schema.mof")
        } else {
            $output = New-Object PSObject |
                Add-Member NoteProperty "$ResourceName.psm1" "$psm1" -PassThru |
                Add-Member NoteProperty "$ResourceName.schema.mof" "$resourceMof" -PassThru

            $output.pstypenames.clear()
            $output.pstypenames.add('cFg.DSC.Resource')
            $output
            
        }
        
        
    }
}