Private/ScopedVariables.ps1

function Add-PodeScopedVariableInternal {
    [CmdletBinding(DefaultParameterSetName = 'Replace')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'Replace')]
        [string]
        $GetReplace,

        [Parameter(ParameterSetName = 'Replace')]
        [string]
        $SetReplace = $null,

        [Parameter(Mandatory = $true, ParameterSetName = 'ScriptBlock')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(ParameterSetName = 'Internal')]
        [switch]
        $InternalFunction
    )

    # lowercase the name
    $Name = $Name.ToLowerInvariant()

    # check if var already defined
    if (Test-PodeScopedVariable -Name $Name) {
        throw ($PodeLocale.scopedVariableAlreadyDefinedExceptionMessage -f $Name)#"Scoped Variable already defined: $($Name)"
    }

    # add scoped var definition
    $PodeContext.Server.ScopedVariables[$Name] = @{
        Name             = $Name
        Type             = $PSCmdlet.ParameterSetName.ToLowerInvariant()
        ScriptBlock      = $ScriptBlock
        Get              = @{
            Pattern = "(?<full>\`$$($Name)\:(?<name>[a-z0-9_\?]+))"
            Replace = $GetReplace
        }
        Set              = @{
            Pattern = "(?<full>\`$$($Name)\:(?<name>[a-z0-9_\?]+)\s*=)"
            Replace = $SetReplace
        }
        InternalFunction = $InternalFunction.IsPresent
    }
}

function Add-PodeScopedVariablesInbuilt {
    Add-PodeScopedVariableInbuiltUsing
    Add-PodeScopedVariableInbuiltCache
    Add-PodeScopedVariableInbuiltSecret
    Add-PodeScopedVariableInbuiltSession
    Add-PodeScopedVariableInbuiltState
}

function Add-PodeScopedVariableInbuiltCache {
    Add-PodeScopedVariable -Name 'cache' `
        -SetReplace "Set-PodeCache -Key '{{name}}' -InputObject " `
        -GetReplace "Get-PodeCache -Key '{{name}}'"
}

function Add-PodeScopedVariableInbuiltSecret {
    Add-PodeScopedVariable -Name 'secret' `
        -SetReplace "Update-PodeSecret -Name '{{name}}' -InputObject " `
        -GetReplace "Get-PodeSecret -Name '{{name}}'"
}

function Add-PodeScopedVariableInbuiltSession {
    Add-PodeScopedVariable -Name 'session' `
        -SetReplace "`$WebEvent.Session.Data.'{{name}}' = " `
        -GetReplace "`$WebEvent.Session.Data.'{{name}}'"
}

function Add-PodeScopedVariableInbuiltState {
    Add-PodeScopedVariable -Name 'state' `
        -SetReplace "Set-PodeState -Name '{{name}}' -Value " `
        -GetReplace "`$PodeContext.Server.State.'{{name}}'.Value"
}

function Add-PodeScopedVariableInbuiltUsing {
    Add-PodeScopedVariableInternal -Name 'using' -InternalFunction
}

function Convert-PodeScopedVariableInbuiltUsing {
    param(
        [Parameter(ValueFromPipeline = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [System.Management.Automation.SessionState]
        $PSSession
    )

    # do nothing if no script or session
    if (($null -eq $ScriptBlock) -or ($null -eq $PSSession)) {
        return $ScriptBlock, $null
    }

    # rename any __using_ vars for inner timers, etcs
    $strScriptBlock = "$($ScriptBlock)"
    $foundInnerUsing = $false

    while ($strScriptBlock -imatch '(?<full>\$__using_(?<name>[a-z0-9_\?]+))') {
        $foundInnerUsing = $true
        $strScriptBlock = $strScriptBlock.Replace($Matches['full'], "`$using:$($Matches['name'])")
    }

    # just return if there are no $using:
    if ($strScriptBlock -inotmatch '\$using:') {
        return $ScriptBlock, $null
    }

    # if we found any inner usings, recreate the scriptblock
    if ($foundInnerUsing) {
        $ScriptBlock = [scriptblock]::Create($strScriptBlock)
    }

    # get any using variables
    $usingVars = Get-PodeScopedVariableUsingVariable -ScriptBlock $ScriptBlock
    if (($null -eq $usingVars) -or ($usingVars.Count -eq 0)) {
        return $ScriptBlock, $null
    }

    # convert any using vars to use new names
    $usingVars = Find-PodeScopedVariableUsingVariableValue -UsingVariable $usingVars -PSSession $PSSession

    # now convert the script
    $newScriptBlock = Convert-PodeScopedVariableUsingVariable -ScriptBlock $ScriptBlock -UsingVariables $usingVars

    # return converted script
    return $newScriptBlock, $usingVars
}

<#
.SYNOPSIS
    Retrieves all occurrences of using variables within a given script block.
 
.DESCRIPTION
    The `Get-PodeScopedVariableUsingVariable` function analyzes a script block and identifies all instances of using variables.
    It returns an array of `UsingExpressionAst` objects representing these occurrences.
 
.PARAMETER ScriptBlock
    Specifies the script block to analyze. This parameter is mandatory.
 
.OUTPUTS
    Returns an array of `UsingExpressionAst` objects representing using variables found in the script block.
 
.EXAMPLE
    # Example usage:
    $scriptBlock = {
        $usingVar1 = "Hello"
        $usingVar2 = "World"
        Write-Host "Using variables: $usingVar1, $usingVar2"
    }
 
    $usingVariables = Get-PodeScopedVariableUsingVariable -ScriptBlock $scriptBlock
    # Process the identified using variables as needed.
 
.NOTES
    This is an internal function and may change in future releases of Pode.
#>

function Get-PodeScopedVariableUsingVariable {
    param(
        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock
    )

    # Analyze the script block AST to find using variables
    return $ScriptBlock.Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.UsingExpressionAst] }, $true)
}

<#
.SYNOPSIS
    Finds and maps using variables within a given script block to their corresponding values.
 
.DESCRIPTION
    The `Find-PodeScopedVariableUsingVariableValue` function analyzes a collection of using variables
    (represented as `UsingExpressionAst` objects) within a script block. It retrieves the values of these
    variables from the specified session state (`$PSSession`) and maps them for further processing.
 
.PARAMETER UsingVariable
    Specifies an array of `UsingExpressionAst` objects representing using variables found in the script block.
    This parameter is mandatory.
 
.PARAMETER PSSession
    Specifies the session state from which to retrieve variable values. This parameter is mandatory.
 
.OUTPUTS
    Returns an array of custom objects, each containing the following properties:
    - `OldName`: The original expression text for the using variable.
    - `NewName`: The modified name for the using variable (prefixed with "__using_").
    - `NewNameWithDollar`: The modified name with a dollar sign prefix (e.g., `$__using_VariableName`).
    - `SubExpressions`: An array of sub-expressions associated with the using variable.
    - `Value`: The value of the using variable retrieved from the session state.
 
.EXAMPLE
    # Example usage:
    $usingVariables = Get-PodeScopedVariableUsingVariable -ScriptBlock $scriptBlock
    $mappedVariables = Find-PodeScopedVariableUsingVariableValue -UsingVariable $usingVariables -PSSession $sessionState
    # Process the mapped variables as needed.
 
.NOTES
    - The function handles both direct using variables and child script using variables (prefixed with "__using_").
    - This is an internal function and may change in future releases of Pode.
#>

function Find-PodeScopedVariableUsingVariableValue {
    param(
        [Parameter(Mandatory = $true)]
        $UsingVariable,

        [Parameter(Mandatory = $true)]
        [System.Management.Automation.SessionState]
        $PSSession
    )

    $mapped = @{}

    foreach ($usingVar in $UsingVariable) {
        # Extract variable name
        $varName = $usingVar.SubExpression.VariablePath.UserPath

        # only retrieve value if new var
        if (!$mapped.ContainsKey($varName)) {
            # get value, or get __using_ value for child scripts
            $value = $PSSession.PSVariable.Get($varName)
            if ([string]::IsNullOrEmpty($value)) {
                $value = $PSSession.PSVariable.Get("__using_$($varName)")
            }

            if ([string]::IsNullOrEmpty($value)) {
                throw ($PodeLocale.valueForUsingVariableNotFoundExceptionMessage -f $varName) #"Value for `$using:$($varName) could not be found"
            }

            # Add to mapped variables
            $mapped[$varName] = @{
                OldName           = $usingVar.SubExpression.Extent.Text
                NewName           = "__using_$($varName)"
                NewNameWithDollar = "`$__using_$($varName)"
                SubExpressions    = @()
                Value             = $value.Value
            }
        }

        # Add the variable's sub-expression for later replacement
        $mapped[$varName].SubExpressions += $usingVar.SubExpression
    }

    return @($mapped.Values)
}

<#
.SYNOPSIS
    Converts a script block by replacing using variables with their corresponding values.
 
.DESCRIPTION
    The `Convert-PodeScopedVariableUsingVariable` function takes a script block and a collection of using variables.
    It replaces the using variables within the script block with their associated values.
 
.PARAMETER ScriptBlock
    Specifies the script block to convert. This parameter is mandatory.
 
.PARAMETER UsingVariables
    Specifies an array of custom objects representing using variables and their values.
    Each object should have the following properties:
    - `OldName`: The original expression text for the using variable.
    - `NewNameWithDollar`: The modified name with a dollar sign prefix (e.g., `$__using_VariableName`).
    - `SubExpressions`: An array of sub-expressions associated with the using variable.
    - `Value`: The value of the using variable.
 
.OUTPUTS
    Returns a new script block with replaced using variables.
 
.EXAMPLE
    # Example usage:
    $usingVariables = @(
        @{
            OldName = '$usingVar1'
            NewNameWithDollar = '$__using_usingVar1'
            SubExpressions = @($usingVar1.SubExpression1, $usingVar1.SubExpression2)
            Value = 'SomeValue1'
        },
        # Add other using variables here...
    )
 
    $convertedScriptBlock = Convert-PodeScopedVariableUsingVariable -ScriptBlock $originalScriptBlock -UsingVariables $usingVariables
    # Use the converted script block as needed.
 
.NOTES
    This is an internal function and may change in future releases of Pode.
#>

function Convert-PodeScopedVariableUsingVariable {
    [CmdletBinding()]
    [OutputType([scriptblock])]
    param(
        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter(Mandatory = $true)]
        [hashtable[]]
        $UsingVariables
    )
    # Create a list of variable expressions for replacement
    $varsList = [System.Collections.Generic.List[System.Management.Automation.Language.VariableExpressionAst]]::new()
    $newParams = [System.Collections.ArrayList]::new()

    foreach ($usingVar in $UsingVariables) {
        foreach ($subExp in $usingVar.SubExpressions) {
            $null = $varsList.Add($subExp)
        }
    }

    # Create a comma-separated list of new parameters
    $null = $newParams.AddRange(@($UsingVariables.NewNameWithDollar))
    $newParams = ($newParams -join ', ')
    $tupleParams = [tuple]::Create($varsList, $newParams)

    # Invoke the internal method to replace variables in the script block
    $bindingFlags = [System.Reflection.BindingFlags]'Default, NonPublic, Instance'
    $_varReplacerMethod = $ScriptBlock.Ast.GetType().GetMethod('GetWithInputHandlingForInvokeCommandImpl', $bindingFlags)
    $convertedScriptBlockStr = $_varReplacerMethod.Invoke($ScriptBlock.Ast, @($tupleParams))

    if (!$ScriptBlock.Ast.ParamBlock) {
        $convertedScriptBlockStr = "param($($newParams))`n$($convertedScriptBlockStr)"
    }

    $convertedScriptBlock = [scriptblock]::Create($convertedScriptBlockStr)

    # Handle cases where the script block starts with '$input |'
    if ($convertedScriptBlock.Ast.EndBlock[0].Statements.Extent.Text.StartsWith('$input |')) {
        $convertedScriptBlockStr = ($convertedScriptBlockStr -ireplace '\$input \|')
        $convertedScriptBlock = [scriptblock]::Create($convertedScriptBlockStr)
    }

    return $convertedScriptBlock
}