Hashtable.psm1

[CmdletBinding()]
param()
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath)
$script:PSModuleInfo = Test-ModuleManifest -Path "$PSScriptRoot\$baseName.psd1"
$script:PSModuleInfo | Format-List | Out-String -Stream | ForEach-Object { Write-Debug $_ }
$scriptName = $script:PSModuleInfo.Name
Write-Debug "[$scriptName] - Importing module"
#region [functions] - [public]
Write-Debug "[$scriptName] - [functions] - [public] - Processing folder"
#region [functions] - [public] - [ConvertFrom-Hashtable]
Write-Debug "[$scriptName] - [functions] - [public] - [ConvertFrom-Hashtable] - Importing"
filter ConvertFrom-Hashtable {
    <#
        .SYNOPSIS
        Converts a hashtable to a PSCustomObject.

        .DESCRIPTION
        Recursively converts a hashtable to a PSCustomObject.
        This function is useful for converting structured data to objects,
        making it easier to work with and manipulate.

        .EXAMPLE
        $hashtable = @{
            Name = 'John Doe'
            Age = 30
            Address = @{
                Street = '123 Main St'
                City = 'Somewhere'
                ZipCode = '12345'
            }
            Occupations = @(
                @{
                    Title = 'Developer'
                    Company = 'TechCorp'
                },
                @{
                    Title = 'Consultant'
                    Company = 'ConsultCorp'
                }
            )
        }
        ConvertFrom-Hashtable -InputObject $hashtable

        Output:
        ```powershell
        Name Value
        ---- -----
        Age 30
        Address @{ZipCode=12345; City=Somewhere; Street=123 Main St}
        Name John Doe
        Occupations {@{Title=Developer; Company=TechCorp}, @{Title=Consultant; Company=ConsultCorp}}
        ```

        Converts the provided hashtable into a PSCustomObject.

        .OUTPUTS
        PSCustomObject

        .NOTES
        A custom object representation of the provided hashtable.
        The returned object preserves the original structure of the input.

        .LINK
        https://psmodule.io/Hashtable/Functions/ConvertFrom-Hashtable
    #>

    [OutputType([PSCustomObject])]
    [CmdletBinding()]
    param(
        # The hashtable to convert to a PSCustomObject.
        [Parameter(Mandatory, ValueFromPipeline)]
        [hashtable] $InputObject
    )

    # Prepare a hashtable to hold properties for the PSCustomObject.
    $props = @{}

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

        if ($value -is [hashtable]) {
            # Recursively convert nested hashtables.
            $props[$key] = $value | ConvertFrom-Hashtable
        } elseif ($value -is [array]) {
            # Check each element: if it's a hashtable, convert it; otherwise, leave it as is.
            $props[$key] = $value | ForEach-Object {
                if ($_ -is [hashtable]) {
                    $_ | ConvertFrom-Hashtable
                } else {
                    $_
                }
            }
        } else {
            # For other types, assign directly.
            $props[$key] = $value
        }
    }

    [pscustomobject]$props
}
Write-Debug "[$scriptName] - [functions] - [public] - [ConvertFrom-Hashtable] - Done"
#endregion [functions] - [public] - [ConvertFrom-Hashtable]
#region [functions] - [public] - [ConvertTo-HashTable]
Write-Debug "[$scriptName] - [functions] - [public] - [ConvertTo-HashTable] - Importing"
filter ConvertTo-Hashtable {
    <#
        .SYNOPSIS
        Converts an object to a hashtable.

        .DESCRIPTION
        Recursively converts an object to a hashtable. This function is useful for converting complex objects
        to hashtables for serialization or other purposes.

        .EXAMPLE
        $object = [PSCustomObject]@{
            Name = 'John Doe'
            Age = 30
            Address = [PSCustomObject]@{
                Street = '123 Main St'
                City = 'Somewhere'
                ZipCode = '12345'
            }
            Occupations = @(
                [PSCustomObject]@{
                    Title = 'Developer'
                    Company = 'TechCorp'
                },
                [PSCustomObject]@{
                    Title = 'Consultant'
                    Company = 'ConsultCorp'
                }
            )
        }
        ConvertTo-Hashtable -InputObject $object

        Output:
        ```powershell
        Name Value
        ---- -----
        Age 30
        Address {[ZipCode, 12345], [City, Somewhere], [Street, 123 Main St]}
        Name John Doe
        Occupations {@{Title=Developer; Company=TechCorp}, @{Title=Consultant; Company=ConsultCorp}}
        ```

        This returns a hashtable representation of the object.

        .OUTPUTS
        hashtable

        .NOTES
        The function returns a hashtable representation of the input object,
        converting complex nested structures recursively.

        .LINK
        https://psmodule.io/ConvertTo/Functions/ConvertTo-Hashtable
    #>

    [OutputType([hashtable])]
    [CmdletBinding()]
    param (
        # The object to convert to a hashtable.
        [Parameter(
            Mandatory,
            ValueFromPipeline
        )]
        [PSObject] $InputObject
    )

    $hashtable = @{}

    # Iterate over each property of the object
    $InputObject.PSObject.Properties | ForEach-Object {
        $propertyName = $_.Name
        $propertyValue = $_.Value

        if ($propertyValue -is [PSObject]) {
            if ($propertyValue -is [Array] -or $propertyValue -is [System.Collections.IEnumerable]) {
                # Handle arrays and enumerables
                $hashtable[$propertyName] = @()
                foreach ($item in $propertyValue) {
                    $hashtable[$propertyName] += ConvertTo-Hashtable -InputObject $item
                }
            } elseif ($propertyValue.PSObject.Properties.Count -gt 0) {
                # Handle nested objects
                $hashtable[$propertyName] = ConvertTo-Hashtable -InputObject $propertyValue
            } else {
                # Handle simple properties
                $hashtable[$propertyName] = $propertyValue
            }
        } else {
            $hashtable[$propertyName] = $propertyValue
        }
    }

    $hashtable
}
Write-Debug "[$scriptName] - [functions] - [public] - [ConvertTo-HashTable] - Done"
#endregion [functions] - [public] - [ConvertTo-HashTable]
#region [functions] - [public] - [Format-Hashtable]
Write-Debug "[$scriptName] - [functions] - [public] - [Format-Hashtable] - Importing"
filter Format-Hashtable {
    <#
        .SYNOPSIS
        Converts a hashtable to its PowerShell code representation.

        .DESCRIPTION
        Recursively converts a hashtable to its PowerShell code representation.
        This function is useful for exporting hashtables to `.psd1` files,
        making it easier to store and retrieve structured data.

        .EXAMPLE
        $hashtable = @{
            Key1 = 'Value1'
            Key2 = @{
                NestedKey1 = 'NestedValue1'
                NestedKey2 = 'NestedValue2'
            }
            Key3 = @(1, 2, 3)
            Key4 = $true
        }
        Format-Hashtable -Hashtable $hashtable

        Output:
        ```powershell
        @{
            Key1 = 'Value1'
            Key2 = @{
                NestedKey1 = 'NestedValue1'
                NestedKey2 = 'NestedValue2'
            }
            Key3 = @(
                1
                2
                3
            )
            Key4 = $true
        }
        ```

        Converts the provided hashtable into a PowerShell-formatted string representation.

        .OUTPUTS
        string

        .NOTES
        A string representation of the given hashtable.
        Useful for serialization and exporting hashtables to files.

        .LINK
        https://psmodule.io/Format/Functions/Format-Hashtable
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param (
        # The hashtable to convert to a PowerShell code representation.
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName
        )]
        [object] $Hashtable,

        # The indentation level for formatting nested structures.
        [Parameter()]
        [int] $IndentLevel = 1
    )

    $indent = ' '
    $lines = @()
    $lines += '@{'
    $levelIndent = $indent * $IndentLevel

    foreach ($key in $Hashtable.Keys) {
        Write-Verbose "Processing key: $key"
        $value = $Hashtable[$key]
        Write-Verbose "Processing value: $value"
        if ($null -eq $value) {
            Write-Verbose "Value type: `$null"
            continue
        }
        Write-Verbose "Value type: $($value.GetType().Name)"
        if (($value -is [System.Collections.Hashtable]) -or ($value -is [System.Collections.Specialized.OrderedDictionary])) {
            $nestedString = Format-Hashtable -Hashtable $value -IndentLevel ($IndentLevel + 1)
            $lines += "$levelIndent$key = $nestedString"
        } elseif ($value -is [System.Management.Automation.PSCustomObject]) {
            $nestedString = Format-Hashtable -Hashtable $value -IndentLevel ($IndentLevel + 1)
            $lines += "$levelIndent$key = $nestedString"
        } elseif ($value -is [System.Management.Automation.PSObject]) {
            $nestedString = Format-Hashtable -Hashtable $value -IndentLevel ($IndentLevel + 1)
            $lines += "$levelIndent$key = $nestedString"
        } elseif ($value -is [bool]) {
            $lines += "$levelIndent$key = `$$($value.ToString().ToLower())"
        } elseif ($value -is [int] -or $value -is [double]) {
            $lines += "$levelIndent$key = $value"
        } elseif ($value -is [array]) {
            if ($value.Count -eq 0) {
                $lines += "$levelIndent$key = @()"
            } else {
                $lines += "$levelIndent$key = @("
                $arrayIndent = $levelIndent + $indent  # Increase indentation for elements inside @(...)

                $value | ForEach-Object {
                    $nestedValue = $_
                    Write-Verbose "Processing array element: $_"
                    Write-Verbose "Element type: $($_.GetType().Name)"
                    if (($nestedValue -is [System.Collections.Hashtable]) -or ($nestedValue -is [System.Collections.Specialized.OrderedDictionary])) {
                        $nestedString = Format-Hashtable -Hashtable $nestedValue -IndentLevel ($IndentLevel + 2)
                        $lines += "$arrayIndent$nestedString"
                    } elseif ($nestedValue -is [bool]) {
                        $lines += "$arrayIndent`$$($nestedValue.ToString().ToLower())"
                    } elseif ($nestedValue -is [int]) {
                        $lines += "$arrayIndent$nestedValue"
                    } else {
                        $lines += "$arrayIndent'$nestedValue'"
                    }
                }
                $arrayIndent = $levelIndent
                $lines += "$arrayIndent)"
            }
        } else {
            $value = $value -replace "('+)", "''" # Escape single quotes in a manifest file
            $lines += "$levelIndent$key = '$value'"
        }
    }
    $levelIndent = $indent * ($IndentLevel - 1)
    $lines += "$levelIndent}"
    return $lines -join [Environment]::NewLine
}
Write-Debug "[$scriptName] - [functions] - [public] - [Format-Hashtable] - Done"
#endregion [functions] - [public] - [Format-Hashtable]
#region [functions] - [public] - [Merge-Hashtable]
Write-Debug "[$scriptName] - [functions] - [public] - [Merge-Hashtable] - Importing"
filter Merge-Hashtable {
    <#
        .SYNOPSIS
        Merges multiple hashtables, applying overrides in sequence.

        .DESCRIPTION
        This function takes a primary hashtable (`$Main`) and merges it with one or more override hashtables (`$Overrides`).
        Overrides are applied in order, with later values replacing earlier ones if the same key exists.
        If the `-Force` switch is used, values will be overridden even if they are empty or `$null`.
        The resulting hashtable is returned.

        .EXAMPLE
        $Main = @{
            Key1 = 'Value1'
            Key2 = 'Value2'
        }
        $Override1 = @{
            Key2 = 'Override2'
        }
        $Override2 = @{
            Key3 = 'Value3'
        }
        $Main | Merge-Hashtable -Overrides $Override1, $Override2

        Output:
        ```powershell
        Name Value
        ---- -----
        Key1 Value1
        Key2 Override2
        Key3 Value3
        ```

        Merges `$Main` with two override hashtables, applying overrides in order.

        .EXAMPLE
        $Main = @{
            Key1 = 'Value1'
            Key2 = 'Value2'
        }
        $Override = @{
            Key2 = ''
            Key3 = 'Value3'
        }
        $Main | Merge-Hashtable -Overrides $Override -Force

        Output:
        ```powershell
        Name Value
        ---- -----
        Key1 Value1
        Key2
        Key3 Value3
        ```

        Forces overriding even if the value is empty.

        .OUTPUTS
        Hashtable

        .NOTES
        A merged hashtable with applied overrides.

        .LINK
        https://psmodule.io/Hashtable/Functions/Merge-Hashtable/
    #>


    [OutputType([Hashtable])]
    [Alias('Join-Hashtable')]
    [CmdletBinding()]
    param (
        # Main hashtable
        [Parameter(Mandatory)]
        [hashtable] $Main,

        # Hashtable with overrides.
        # Providing a list of overrides will apply them in order.
        # Last write wins.
        [Parameter(
            Mandatory,
            ValueFromPipeline
        )]
        [hashtable[]] $Overrides,

        # When specified, force override even if the value is empty or null.
        [Parameter()]
        [switch] $Force
    )

    begin {
        $Output = $Main.Clone()
    }

    process {
        foreach ($Override in $Overrides) {
            foreach ($Key in $Override.Keys) {
                if (($Output.Keys) -notcontains $Key) {
                    $Output.$Key = $Override.$Key
                }
                if ($Force -or -not [string]::IsNullOrEmpty($Override[$Key])) {
                    $Output[$Key] = $Override[$Key]
                }
            }
        }
    }

    end {
        return $Output
    }
}
Write-Debug "[$scriptName] - [functions] - [public] - [Merge-Hashtable] - Done"
#endregion [functions] - [public] - [Merge-Hashtable]
#region [functions] - [public] - [Remove-HashtableEntry]
Write-Debug "[$scriptName] - [functions] - [public] - [Remove-HashtableEntry] - Importing"
filter Remove-HashtableEntry {
    <#
        .SYNOPSIS
        Removes specific entries from a hashtable based on value, type, or name.

        .DESCRIPTION
        This version applies keep filters with the highest precedence. If a key
        qualifies based on the provided Keep parameters (KeepTypes and/or KeepKeys),
        it is preserved no matter what removal conditions might say.

        If no keep filters are provided, the function applies removal conditions:
        - NullOrEmptyValues: Remove keys with null or empty values.
        - RemoveTypes: Remove keys whose values are of the specified type(s).
        - RemoveKeys: Remove keys with the specified name(s).

        When Keep filters are provided, only keys that match ALL specified keep criteria
        will be preserved; keys that do not match are removed regardless of removal settings.

        At the end, the original hashtable is cleared and repopulated with the filtered results.

        .EXAMPLE
        $ht = @{
            KeepThis = 'Value1'
            RemoveThis = 'Delete'
            Other = 42
        }
        $ht | Remove-HashtableEntry -KeepKeys 'KeepThis' -RemoveKeys 'RemoveThis'

        This will keep only the key "KeepThis", regardless of other removal flags.

        .OUTPUTS
        hashtable

        .NOTES
        The function modifies the input hashtable in place.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Function does not change state.'
    )]
    [OutputType([void])]
    [CmdletBinding()]
    param(
        # The hashtable to remove entries from.
        [Parameter(Mandatory, ValueFromPipeline)]
        [hashtable] $Hashtable,

        # Remove keys with null or empty values.
        [Parameter()]
        [switch] $NullOrEmptyValues,

        # Remove keys of a specified type.
        [Parameter()]
        [string[]] $Types,

        # Remove keys with a specified name.
        [Parameter()]
        [Alias('Names')]
        [string[]] $Keys,

        # Remove keys with null or empty values.
        [Parameter()]
        [Alias('IgnoreNullOrEmptyValues')]
        [switch] $KeepNullOrEmptyValues,

        # Keep only keys of a specified type.
        [Parameter()]
        [Alias('IgnoreTypes')]
        [string[]] $KeepTypes,

        # Keep only keys with a specified name.
        [Parameter()]
        [Alias('IgnoreKey', 'KeepNames')]
        [string[]] $KeepKeys,

        # Remove all entries from the hashtable.
        [Parameter()]
        [switch] $All
    )

    # Copy keys to a static array to prevent modifying the collection during iteration.
    $hashtableKeys = @($Hashtable.Keys)
    foreach ($key in $hashtableKeys) {
        $value = $Hashtable[$key]
        $vaultIsNullOrEmpty = [string]::IsNullOrEmpty($value)
        $valueIsNotNullOrEmpty = -not $vaultIsNullOrEmpty
        $typeName = if ($valueIsNotNullOrEmpty) { $value.GetType().Name } else { $null }

        if ($KeepKeys -and $key -in $KeepKeys) {
            Write-Debug "Keeping [$key] because it is in KeepKeys [$KeepKeys]."
        } elseif ($KeepTypes -and $typeName -in $KeepTypes) {
            Write-Debug "Keeping [$key] because its type [$typeName] is in KeepTypes [$KeepTypes]."
        } elseif ($vaultIsNullOrEmpty -and $KeepNullOrEmptyValues) {
            Write-Debug "Keeping [$key] because its value is null or empty."
        } elseif ($vaultIsNullOrEmpty -and $NullOrEmptyValues) {
            Write-Debug "Removing [$key] because its value is null or empty."
            $Hashtable.Remove($key)
        } elseif ($Types -and $typeName -in $Types) {
            Write-Debug "Removing [$key] because its type [$typeName] is in Types [$Types]."
            $Hashtable.Remove($key)
        } elseif ($Keys -and $key -in $Keys) {
            Write-Debug "Removing [$key] because it is in Keys [$Keys]."
            $Hashtable.Remove($key)
        } elseif ($All) {
            Write-Debug "Removing [$key] because All flag is set."
            $Hashtable.Remove($key)
        } else {
            Write-Debug "Keeping [$key] by default."
        }
    }
}
Write-Debug "[$scriptName] - [functions] - [public] - [Remove-HashtableEntry] - Done"
#endregion [functions] - [public] - [Remove-HashtableEntry]
Write-Debug "[$scriptName] - [functions] - [public] - Done"
#endregion [functions] - [public]

#region Member exporter
$exports = @{
    Alias    = '*'
    Cmdlet   = ''
    Function = @(
        'ConvertFrom-Hashtable'
        'ConvertTo-HashTable'
        'Format-Hashtable'
        'Merge-Hashtable'
        'Remove-HashtableEntry'
    )
}
Export-ModuleMember @exports
#endregion Member exporter