Includes/PwSh.Fw.Object.psm1

$Script:NS = "PwSh.Object"

<#
.SYNOPSIS
Convert an XML content to a PowerShell Object
 
.DESCRIPTION
Convert any XML content to a PSCustomObject. It can then be manipulated like any object.
It handles array and nested XML content.
It is usefull for example to convert an XML object to a JSON object
 
.PARAMETER InputObject
XML object to convert
 
.EXAMPLE
[XML]$xml = Get-Content /path/to/file.xml
$obj = $xml | ConvertFrom-Xml
$json = $obj | ConvertTo-Json
 
.NOTES
 
#>


function ConvertFrom-Xml {
    [CmdletBinding()]Param (
        [Parameter(Mandatory = $true,ValueFromPipeLine = $true)][System.Object]$InputObject
    )
    Begin {
        # eenter($Script:NS + "\" + $MyInvocation.MyCommand)
    }

    Process {
        $OutputObject = New-Object PSObject
        if ($null -ne $InputObject) {
            if ($InputObject.HasAttributes) {
                ForEach ($attr in $InputObject.Attributes) {
                    $OutputObject | Add-Member -MemberType NoteProperty -Name $attr.Name -Value $attr.Value
                }
            }
            if ($InputObject.HasChildNodes) {
                ForEach ($child in $InputObject.ChildNodes) {
                    $OutputObject | Add-Member -MemberType NoteProperty -Name $child.LocalName -Value @() -ErrorAction SilentlyContinue
                    $OutputObject.($child.LocalName) += ($child | ConvertFrom-Xml)
                }
            }
        }
        return $OutputObject
    }

    End {
        # eleave($Script:NS + "\" + $MyInvocation.MyCommand)
    }
}

<#
.SYNOPSIS
List object's properties.
 
.DESCRIPTION
Get properties of the type of an object.
It can be used to filter out default object's type properties.
 
.PARAMETER obj
Object of reference
 
.EXAMPLE
$s = "this is a test"
$s | Get-ObjectProperties
 
.NOTES
#>

function Get-ObjectProperties {
    [CmdletBinding()]Param (
        [Parameter(Mandatory = $true,ValueFromPipeLine = $true)][System.object]$obj,
        [string[]]$Exclude
    )
    Begin {
        # eenter($Script:NS + "\" + $MyInvocation.MyCommand)
    }

    Process {
        if (!$obj) { return }
        if ($null -eq $obj) { return }

        Try    {
            $DefaultTypeProps = @( $obj.gettype().GetProperties() | Where-Object { $_.Name -notin $Exclude } | Select-Object -ExpandProperty Name -ErrorAction Stop )
            if ($DefaultTypeProps.count -gt 0) {
                # edevel("Excluding default properties for $($obj.gettype().Fullname):")
                # edevel($($DefaultTypeProps | Out-String))
            }
        }
        Catch {
            ewarn("Failed to extract properties from $($obj.gettype().Fullname): $_")
            $DefaultTypeProps = @()
        }

        @( $DefaultTypeProps ) | Select-Object -Unique
    }

    End {
        # eleave($Script:NS + "\" + $MyInvocation.MyCommand)
    }
}

<#
.SYNOPSIS
Get the usefull properties of an object.
 
.DESCRIPTION
Get the properties of an object minus the default object properties. It is the opposite of Get-ObjectProperties.
 
.PARAMETER InputObject
Object to inspect
 
.PARAMETER Include
For inclusion of a default property that would otherwise been stripped out.
 
.EXAMPLE
$obj | Get-CustomObjectProperties
 
.EXAMPLE
$obj | Get-CustomObjectProperties -Include Name
 
.NOTES
General notes
#>


function Get-CustomObjectProperties {
    [CmdletBinding()]Param (
        [Parameter(Mandatory = $true,ValueFromPipeLine = $true)][Object]$InputObject,
        [string[]]$Include
    )
    Begin {
        # eenter($Script:NS + '\' + $MyInvocation.MyCommand)
    }

    Process {
        $excludeProps = $InputObject | Get-ObjectProperties | Where-Object { $_ -notin $Include }
        Try    {
            $DefaultTypeProps = @($InputObject.gettype().GetProperties() | Where-Object { $_.Name -notin $excludeProps } | Select-Object -ExpandProperty Name -ErrorAction Stop )
            if ($DefaultTypeProps.count -gt 0) {
                # edevel($($DefaultTypeProps | Out-String))
            }
        } Catch {
            ewarn("Failed to extract properties from $($obj.gettype().Fullname): $_")
            $DefaultTypeProps = @()
        }

        return @($DefaultTypeProps) | Sort-Object -Unique
    }

    End {
        # eleave($Script:NS + '\' + $MyInvocation.MyCommand)
    }
}

function Merge-Object {
    [CmdletBinding()]Param (
        [Parameter(Mandatory = $true,ValueFromPipeLine = $true)][Object]$InputObject1,
        [Parameter(Mandatory = $true,ValueFromPipeLine = $false)][Object]$InputObject2
    )
    Begin {
        # eenter($Script:NS + '\' + $MyInvocation.MyCommand)
    }

    Process {
        $excludeProps = $InputObject2 | Get-ObjectProperties

        return $OutputObject
    }

    End {
        # eleave($Script:NS + '\' + $MyInvocation.MyCommand)
    }
}

<#
.SYNOPSIS
Merge two or more hashtables
 
.DESCRIPTION
Merge multiple hashtables into one.
For this cmdlet you can use several syntaxes and you are not limited to two input tables: Using the pipeline: $h1, $h2, $h3 | Merge-Hashtables
Using arguments: Merge-Hashtables $h1 $h2 $h3
Or a combination: $h1 | Merge-Hashtables $h2 $h3
 
.EXAMPLE
$h1, $h2, $h3 | Merge-Hashtables
 
.EXAMPLE
Merge-Hashtables $h1 $h2 $h3
 
.EXAMPLE
$h1 | Merge-Hashtables $h2 $h3
 
.NOTES
https://stackoverflow.com/questions/8800375/merging-hashtables-in-powershell-how
#>


Function Merge-Hashtables {
    $Output = @{}
    ForEach ($Hashtable in ($Input + $Args)) {
        If ($Hashtable -is [Hashtable]) {
            ForEach ($Key in $Hashtable.Keys) {$Output.$Key = $Hashtable.$Key}
        }
    }
    $Output
}

<#
.SYNOPSIS
Sort a hashtable
 
.DESCRIPTION
Sort a hashtable by Name
 
.PARAMETER InputObject
Hashtable to sort
 
.EXAMPLE
$h = @{ "this" = "is"; "a" = "test"}
$h | Sort-HashTable
 
.NOTES
I know Sort is not an approved verb but hey, this function actually DOES sort a hashtable
 
.OUTPUTS
The sorted hashtable
#>


function Sort-HashTable {
    [CmdletBinding()]Param (
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][hashtable]$InputObject
    )
    Begin {
        # eenter($Script:NS + '\' + $MyInvocation.MyCommand)
    }

    Process {
        # return $InputObject.GetEnumerator() | Sort-Object -Property Name
        $hReturn = [ordered]@{}
        $InputObject.GetEnumerator() | Sort-Object -Property Name | ForEach-Object {
            $hReturn.Add($_.Name, $_.Value)
        }
        return $hReturn
    }

    End {
        # eleave($Script:NS + '\' + $MyInvocation.MyCommand)
    }
}

<#
.SYNOPSIS
    Convert a XML Plist to a PowerShell object
 
.DESCRIPTION
    Converts an XML PList (property list) in to a usable object in PowerShell.
    Properties will be converted in to ordered hashtables, the values of each property may be integer, double, date/time, boolean, string, or hashtables, arrays of any these, or arrays of bytes.
 
.PARAMETER plist
    The property list as an [XML] document object, to be processed. This parameter is mandatory and is accepted from the pipeline.
 
.EXAMPLE
    $pList = [xml](Get-Content 'somefile.plist') | ConvertFrom-Plist
 
.INPUTS
    system.xml.document
 
.OUTPUTS
    system.object
 
.NOTES
    Original Script / Function / Class assembled by Carl Morris, Morris Softronics, Hooper, NE, USA
    Initial release - Aug 27, 2018
    Rewritten without the use of class
 
.LINK
    https://github.com/msftrncs/PwshReadXmlPList
 
.FUNCTIONALITY
    data format conversion
#>

function ConvertFrom-Plist {
    [CmdletBinding()]Param (
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][xml]$plist
    )
    Begin {
        # eenter($Script:NS + '\' + $MyInvocation.MyCommand)
    }

    Process {
        if ($null -eq $plist.item('plist')) {
            return $null
        } else {
            return (Read-PlistNode -Node $plist.item('plist').FirstChild)
        }
    }

    End {
        # eleave($Script:NS + '\' + $MyInvocation.MyCommand)
    }
}

function Read-PlistNode {
    [CmdletBinding()][OutputType([System.Object])]Param (
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][System.Xml.XmlElement]$node
    )
    Begin {
        # eenter($Script:NS + '\' + $MyInvocation.MyCommand)
    }

    Process {
        if ($node.HasChildNodes) {
            # edevel "$($node.name) - $($node.'#text')"
            switch ($node.Name) {
                array {
                    # for arrays, recurse each node in the subtree, returning an array (forced)
                    , @($node.ChildNodes.foreach{ (Read-PlistNode -Node $_) })
                    continue
                }
                date {
                    # must be a date-time type value element, return its value
                    $node.InnerText -as [datetime]
                    continue
                }
                data {
                    # must be a data block value element, return its value as [byte[]]
                    # [convert]::FromBase64String((Read-PlistNode -Node $node.InnerText))
                    $node.InnerText
                    continue
                }
                dict {
                    # for dictionary, return the subtree as a ordered hashtable, with possible recursion of additional arrays or dictionaries
                    $collection = [ordered]@{}
                    $currnode = $node.FirstChild # start at the first child node of the dictionary
                    while ($null -ne $currnode) {
                        if ($currnode.Name -eq 'key') {
                            # edevel "$($currnode.name) - $($currnode.'#text')"
                            # a key in a dictionary, add it to a collection
                            if ($null -ne $currnode.NextSibling) {
                                # edevel "$($currnode.NextSibling.name) - $($currnode.NextSibling.'#text')"
                                # note: keys are forced to [string], insures a $null key is accepted
                                # $collection[$currnode.InnerText] = (Read-PlistNode -Node $currnode.NextSibling)
                                $collection.Add($currnode.InnerText, (Read-PlistNode -Node $currnode.NextSibling))
                                $currnode = $currnode.NextSibling.NextSibling # skip the next sibling because it was the value of the property
                            } else {
                                throw "Dictionary property value missing!"
                            }
                        } else {
                            throw "Non 'key' element found in dictionary: <$($currnode.Name)>!"
                        }
                    }
                    # return the collected hash table
                    $collection
                    continue
                }
                integer {
                    # must be an integer type value element, return its value
                    $node.InnerText -as [int]
                    continue
                }
                real {
                    $node.InnerText -as [double]
                    continue
                }
                string {
                    # for string, return the value, with possible recursion and
                    # collection
                    $node.InnerText
                    continue
                }
                default {
                    # we didn't recognize the element type!
                    throw "Unhandled PLIST property type <$($node.Name)>!"
                }
            }
        } else {
            # return simple element value (need to check for Boolean datatype, and process value accordingly)
            switch ($node.Name) {
                true { $true; continue } # return a Boolean TRUE value
                false { $false; continue } # return a Boolean FALSE value
                # default { $node.Value } # return the element value
            }
        }
    }

    End {
        # eleave($Script:NS + '\' + $MyInvocation.MyCommand)
    }
}

function ConvertTo-CamelCase {
    [CmdletBinding()]Param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [AllowNull()][AllowEmptyString()]
        [string]$String
    )
    Begin {
        # eenter($MyInvocation.MyCommand)
    }

    Process {
        # convert to Title Case
        $camelCase = (get-culture).TextInfo.ToTitleCase($String)
        # transforme accent to normal letters
        $camelCase = $camelCase | Remove-StringLatinCharacters
        # remove non alphanumeric characters
        $camelCase = $camelCase -replace '[^a-zA-Z0-9]', ''
        # convert 1st letter to lowercase
        $camelCase = $camelCase.Substring(0,1).ToLower() + $camelCase.Substring(1)

        return $camelCase
    }

    End {
        # eleave($MyInvocation.MyCommand)
    }
}

<#
    .SYNOPSIS
    Serialize an object to a single string
 
    .DESCRIPTION
    Convert an object to a single string to ease display and debug
 
    .PARAMETER InputObject
    an object (currently, only hashtable are supported)
 
    .EXAMPLE
    $ht = @{'key'="value";'key2'="value2"}
    This defines a hastable
 
    .EXAMPLE
    $ht | ConvertTo-SingleString
    This example convert the previously created hastable into a single, serialized string
 
    .NOTES
    General notes
#>

function ConvertTo-SingleString {
    [CmdletBinding()]param(
        [parameter(Mandatory,ValueFromPipeline = $True)]
        $InputObject
    )

    Begin {
    }

    Process {
        # $InputObject.GetTYpe()
        switch ($InputObject.GetTYpe()) {
            'Hashtable' {
                # $serialized = ($InputObject.GetEnumerator() | % { "'$($_.Key)'=`"$($_.Value)`"" }) -join ';'
                $serialized = Foreach ($k in $InputObject.GetEnumerator()) {
                    switch ($k.Value.GetType()) {
                        'datetime' {
                            "'$($k.Key)'=[System.DateTime]`"$($k.Value)`""
                        }
                        default {
                            "'$($k.Key)'=`"$($k.Value)`""
                        }
                    }
                }
                $serialized = $serialized -join(';')
            }
            default {
                eerror("Object type '" + $InputObject.GetTYpe() + "' not supported yet.")
                return $false
            }
        }
        return "@{" + $serialized + "}"
    }

    End {
    }
}

<#
    .LINK
    http://www.lazywinadmin.com/2015/05/powershell-remove-diacritics-accents.html
#>

function Remove-StringLatinCharacters
{
    PARAM (
        [parameter(ValueFromPipeline = $true)]
        [string]$String
    )
    PROCESS
    {
        [Text.Encoding]::ASCII.GetString([Text.Encoding]::GetEncoding("Cyrillic").GetBytes($String))
    }
}

<#
.SYNOPSIS
Resolve boolean well-known values
 
.DESCRIPTION
Boolean are not just (true | false) value. It can by yes/no or 0/1. Resolve-Boolean handle all of this.
 
.PARAMETER var
The variable name to check
 
.EXAMPLE
true | Resolve-Boolean
 
.EXAMPLE
0 | Resolve-Boolean
 
.EXAMPLE
if ((Resolve-Boolean -var "yes") -eq $true) { echo "yes is true" }
 
.NOTES
General notes
#>


function Resolve-Boolean {
    [CmdletBinding()][OutputType([boolean])]Param (
        [Parameter(Mandatory,ValueFromPipeLine = $true)]$var
    )
    Begin {
        # eenter($MyInvocation.MyCommand)
    }

    Process {
        switch -regex ($var.GetType()) {
            'bool*' {
                return $var
            }
            'int*' {
                switch ($var) {
                    0         { return $false }
                    1         { return $true  }
                }
            }
            'string' {
                switch -wildcard ($var) {
                    'false' { return $false }
                    'true'    { return $true  }
                    'n*'    { return $false }
                    'y*'    { return $true  }
                }
            }
        }
        return $false
    }

    End {
        # eleave($MyInvocation.MyCommand)
    }
}

Set-Alias -Force -Name Resolve-Bool -Value Resolve-Boolean