DSCToXTA.psm1

function Get-DSCVariables
{
    [CmdletBinding()]
    [OutputType([System.String[]])]
    param(
        [Parameter()]
        [System.String]
        $Content
    )

    $Tokens = $null
    $ParseErrors = $null
    $AST = [System.Management.Automation.Language.Parser]::ParseInput($Content, [ref]$Tokens, [ref]$ParseErrors)

    $variables = @()
    foreach ($token in $Tokens)
    {
        if ($token.Kind -eq 'Variable')
        {
            $variables += $token.Extent.Text
        }
    }

    $variablesToExclude = @('$null', '$false', '$true', '$_')

    # sort variable by length descending to avoid partial matches
    return $variables | Where-Object { $_ -notin $variablesToExclude } | Select-Object -Unique | Sort-Object -Property Length -Descending

}

function Format-XTAProperty
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param(
        [Parameter()]
        [System.String]
        $Property,

        [Parameter()]
        [System.String[]]
        $Variables
    )

    foreach($variable in $Variables)
    {
        # matches params of the type : [parameters('FQDN')] where the parameter value is used as a single value.
        if (($Property -eq $variable) -or ($Property -eq "`$($variable)"))
        {
            $Property = "[parameters('$($variable.Substring(1))')]"
            return $Property
        }
    }

    # matches params of the type : [concat('admin_', parameters('FQDN'), '@', parameters('Domain'), '.com')] where the parameter value is used within a list.
    # Replace all variables with ,parameters('variableName'), and then split the string by ',' and then join the string with concat
    $hasVariable = $false
    foreach($variable in $Variables) 
    {
        $hasVariable = $hasVariable -or $Property.Contains($variable)

        # Replace doesn't work well with special characters, so we need to escape special characters the variable
        $bracketVariable = "`$($variable)"
        $escapedBracketVariable = [regex]::Escape($bracketVariable)
        $property = $Property -replace $escapedBracketVariable, ",parameters('$($variable.Substring(1))'),"

        $escapedVariable = [regex]::Escape($variable)
        $property = $Property -replace $escapedVariable, ",parameters('$($variable.Substring(1))'),"
    }

    if($hasVariable)
    {
        $splits = @()
        
        $property.Split(",") | ForEach-Object {
            if($_ -ne "")
            {
                if($_ -match "parameters\('(.*)'\)")
                {
                    $splits += $_
                }
                else
                {
                    $splits += "'$_'"
                }
            }
        }
        $property = "[concat($($splits -join ', '))]"
    }

    return $Property
}

function Format-XTAProperties
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param(
        [Parameter()]
        [System.Collections.Hashtable]
        $Resource,

        [Parameter()]
        [System.String[]]
        $Variables
    )

    $ParsedResource = @{}

    foreach ($key in $Resource.Keys)
    {
        $value = $Resource[$key]
        $parsedValue = $value

        if ($value -is [System.String])
        {
            $parsedValue = Format-XTAProperty -Property $value -Variables $Variables
        }
        elseif ($value -is [System.Collections.Hashtable])
        {
            $parsedValue = Format-XTAProperties -Resource $value -Variables $Variables
        }
        elseif ($value -is [System.Collections.ArrayList] -or $value -is [System.Array] -or $value -is [System.Collections.Generic.List[System.Object]])
        {
            $parsedValue = @()
            foreach ($item in $value)
            {
                if ($item -is [System.String])
                {
                    $parsedItem = Format-XTAProperty -Property $item -Variables $Variables
                    $parsedValue += $parsedItem
                }
                elseif ($item -is [System.Collections.Hashtable])
                {
                    $parsedItem = Format-XTAProperties -Resource $item -Variables $Variables
                    $parsedValue += $parsedItem
                }
            }
        }

        $ParsedResource.Add($key, $parsedValue)
    }

    return $ParsedResource
}

function ConvertFrom-DSCToXTA
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param(
        [Parameter()]
        [System.String]
        $Path,

        [Parameter()]
        [System.String]
        $Content,

        [Parameter()]
        [System.Boolean]
        $Compress = $false,

        [Parameter()]
        [System.Boolean]
        $Parameterize = $true
    )
    Write-Warning "
        Please note that the script doesn’t support converting Microsoft365DSC files that:
            1. Include PowerShell conditional logic (if/else),
            2. Include PowerShell looping logic (for/while),
            3. Is made up of DSC Composites (https://learn.microsoft.com/en-us/powershell/dsc/resources/authoringresourcecomposite?view=dsc-1.1)
            4. Include non string variables, or
            5. Include null values in the configuration
    "

    # Initialization - Skip
    $Global:M365DSCSkipDependenciesValidation = $true

    # Initialization - Load the Mapping Information
    $mappingPath = Join-Path -Path $PSScriptRoot -ChildPath 'DSCToXTAMappings.psd1' -Resolve
    $mappings = Import-PowerShellDataFile $mappingPath

    # Initialization - Load the XTA Template
    $templatePath = Join-Path -Path $PSScriptRoot -ChildPath 'XTATemplate.json' -Resolve
    $templateContent = Get-Content -Path $templatePath -Raw
    $template = ConvertFrom-JSON $templateContent
    
    # If a path to a file is provided, then get its raw content
    if ([System.String]::IsNullOrEmpty($Content) -and -not [System.String]::IsNullOrEmpty($Path))
    {
        $Content = Get-Content -Path $Path -Raw
    }

    # Use the DSCParser to convert the file's content into an array of
    # PowerShell objects (Hashtables).
    $parsedContent = ConvertTo-DSCObject -Content $Content `
                                         -IncludeCIMInstanceInfo $false

    # Get all the variables used in the DSC configuration
    $variables = Get-DSCVariables -Content $Content

    # Add the variables as parameters to the XTA template
    foreach ($variable in $variables)
    {
        $variableName = $variable.Substring(1)
        $template.Parameters += @{
            displayName = $variableName
            description = "The declaration of variable $variable."
            parameterType = "String"
        }
    }

    # Loop through all the resources and convert them to XTA
    $allResources = @()
    foreach ($resource in $parsedContent)
    {
        $mappedNamespace = $mappings.($resource.ResourceName)
        if (-not [System.String]::IsNullOrEmpty($mappedNamespace))
        {
            $ResourceInstanceName = $resource.ResourceInstanceName

            $resource.Remove("ResourceInstanceName") | Out-Null
            $resource.Remove("ResourceName") | Out-Null
            $resource.Remove("Credential") | Out-Null
            $resource.Remove("ApplicationId") | Out-Null
            $resource.Remove("TenantId") | Out-Null
            $resource.Remove("CertificateThumbprint") | Out-Null
            $resource.Remove("ApplicationSecret") | Out-Null
            $resource.Remove("CertificatePath") | Out-Null
            $resource.Remove("CertificatePassword") | Out-Null

            if($Parameterize)
            {
                foreach ($Variable in $Variables) {
                    if ($ResourceInstanceName -like "*$Variable*") {
                        $ResourceInstanceName  = Format-XTAProperty `
                        -Property $ResourceInstanceName -Variables $Variable
                        break
                    }
                }
                $resource = Format-XTAProperties -Resource $resource -Variables $variables
            }

            $currentResource = @{
                displayname = $ResourceInstanceName
                resourceType = $mappedNamespace
            }
            $currentResource.Add("properties", $resource)
            $allResources += $currentResource
        }
    }
    $template.Resources = $allResources
    $json = (ConvertTo-Json $template -Depth 99 -Compress:$Compress)
    return $json.Replace("\u0027", "'")
}