ConvertToPsd1.psm1

function ConvertTo-Psd1 {
    <#
    .SYNOPSIS
        Converts objects into psd1 string.
     
    .DESCRIPTION
        Converts objects into psd1 string.
     
    .PARAMETER InputObject
        The item(s) to convert
     
    .PARAMETER Depth
        How deeply nested information will be picked up.
        This command will automatically prevent infinite recursion, even without the -Depth parameter.
     
    .PARAMETER WriteError
        If the object in the input file has any issues, should an error be generated?
        Otherwise, just a warning will be given.
        Note: ErrorAction stop will always lead to terminating errors in case of parsing issues.
     
    .EXAMPLE
        PS C:\> Get-ChildItem | ConvertTo-Psd1
 
        Converts all files & folders in the current path to a psd1-string representing its contents.
        Each file processed separately.
    #>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true)]
        [object[]]
        $InputObject,

        [int]
        $Depth,

        [switch]
        $WriteError
    )
    begin {
        $converter = [converter]::new($PSCmdlet, $WriteError, ($ErrorActionPreference -eq 'Stop'), $Depth)
    }
    process {
        foreach ($item in $InputObject) {
            $converter.Convert($item)
        }
    }
}

function ConvertTo-Psd1File {
    <#
    .SYNOPSIS
        Converts json files to psd1.
     
    .DESCRIPTION
        Converts json files to psd1.
        The psd1 file will be placed in the same path under the same name with just the extension updated.
     
    .PARAMETER Path
        Path to the files to convert.
 
    .PARAMETER OutPath
        Path where the resultant file should be placed.
        By default, it will be placed in the same path as the source file.
 
    .PARAMETER Depth
        How deeply nested information will be picked up.
        This command will automatically prevent infinite recursion, even without the -Depth parameter.
     
    .PARAMETER WriteError
        If the object in the input file has any issues, should an error be generated?
        Otherwise, just a warning will be given.
        Note: ErrorAction stop will always lead to terminating errors in case of parsing issues.
     
    .EXAMPLE
        PS C:\> Get-ChildItem -Path . -Filter *.json | ConvertTo-Psd1File
 
        Converts all json files in the current directory to psd1.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('FullName')]
        [string[]]
        $Path,

        [string]
        $OutPath,

        [int]
        $Depth,

        [switch]
        $WriteError
    )
    
    begin {
        $converter = [converter]::new($PSCmdlet, $WriteError, ($ErrorActionPreference -eq 'Stop'), $Depth)
    }
    process {
        foreach ($filePath in $Path) {
            $item = Get-Item -Path $filePath
            $data = Get-Content -LiteralPath $item.FullName | ConvertFrom-Json
            $lines = foreach ($entry in $data) {
                $converter.Convert($entry)
            }
            if ($OutPath) { $exportPath = Join-Path -Path $OutPath -ChildPath "$($item.BaseName).psd1" }
            else { $exportPath = Join-Path -Path $item.DirectoryName -ChildPath "$($item.BaseName).psd1" }
            $lines | Set-Content -LiteralPath $exportPath
        }
    }
}

class Converter {
    [object] $Cmdlet
    [bool] $ThrowOnUnknown
    [bool] $Terminate
    [int] $Depth

    Converter([object]$Cmdlet, [bool] $ThrowOnUnknown, [bool] $Terminate, [int]$Depth) {
        $this.Cmdlet = $Cmdlet
        $this.ThrowOnUnknown = $ThrowOnUnknown
        $this.Terminate = $Terminate
        $this.Depth = $Depth
    }

    [string] ConvertValue([object] $Value, [object[]]$Parents, [int]$Depth) {
        # Those number-thingies
        if ($Value -is [int] -or $Value -is [long] -or $Value -is [double]) {
            return '{0}' -f $Value
        }

        # Case: Bool
        if ($Value -is [bool] -or $Value -is [System.Management.Automation.SwitchParameter]) {
            return '${0}' -f $Value
        }

        # Case: Null
        if ($null -eq $Value -or $Value -is [System.DBNull]) {
            return '$null'
        }

        # Case: DateTime
        if ($Value -is [datetime]) {
            return "'{0:yyyy-MM-dd HH:mm:ss.fffff zzz}'" -f $Value.ToUniversalTime()
        }

        # Case: Guid
        if ($Value -is [guid]) {
            return "'$Value'"
        }

        # Case: Version
        if ($Value -is [version]) {
            return "'$Value'"
        }

        # Case: String
        if ($Value -is [string] -or $Value -is [char] -or $Value -is [System.Uri]) {
            return '''{0}''' -f ([System.Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($Value))
        }

        # Case: Hashtable
        if ($Value -is [System.Collections.IDictionary]) {
            return $this.ConvertHashtable($Value, $Parents, $Depth)
        }

        # Case: IEnumerable
        if ($Value -is [System.Collections.IEnumerable]) {
            $oldIndent = ' ' * $Depth
            $newIndent = ' ' * ($Depth + 1)
            $newParent = @($Parents) + ,$Value
            $pieces = @("@(")
            
            foreach ($entry in $Value) {
                $pieces += $newIndent + $this.ConvertValue($entry, $newParent, $Depth)
            }

            $pieces += "$oldIndent)"
            return $pieces -join "`n"
        }

        # Case: Enum
        if ($Value -is [enum]) {
            return "'{0}'" -f ([System.Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($Value))
        }

        # Case: Assembly
        if ($Value -is [System.Reflection.Assembly] -or $Value -is [System.Reflection.TypeInfo]) {
            return "'{0}'" -f $Value.FullName
        }

        if ($Value -is [System.Management.Automation.ProviderInfo] -or $Value -is [System.Management.Automation.PSDriveInfo]) {
            return "'{0}'" -f $Value.Name
        }

        # Case: PSCustomObject
        if ($Value -is [PSCustomObject] -or $Value.PSObject.Properties.Count -gt 0) {
            return $this.ConvertPSCustomObject($Value, $Parents, $Depth)
        }

        $message = "Unexpected data entry: $Value ($($Value.GetType().FullName))"
        if ($this.ThrowOnUnknown -or $this.Terminate) {
            if ($null -eq $this.Cmdlet) {
                throw $message
            }

            $record = [System.Management.Automation.ErrorRecord]::new(
                [System.Management.Automation.ParseException]::new($message),
                'BadData',
                [System.Management.Automation.ErrorCategory]::ParserError,
                $Value
            )
            if ($this.Terminate) {
                $this.Cmdlet.ThrowTerminatingError($record)
            }
            $this.Cmdlet.WriteError($record)
        }
        $this.Cmdlet.WriteWarning($message)
        return "'{0}'" -f ("$Value" -replace "'", "''")
    }

    [string] ConvertHashtable([System.Collections.IDictionary]$Value, [object[]]$Parents, [int]$Depth) {
        if ($Value -in $Parents) { return "'System.Collections.Hashtable (recursed)'" }
        $newDepth = $Depth + 1
        if ($this.Depth -gt 0 -and $newDepth -gt $this.Depth) { return "'System.Collections.Hashtable'" }
        $oldIndent = ' ' * $Depth
        $newIndent = ' ' * $newDepth
        
        $newParents = @($Parents) + $Value
        $lines = @("$oldIndent@{")
        foreach ($entry in $Value.GetEnumerator()) {
            if ($Parents[-1] -is [System.IO.FileSystemInfo] -and $entry.Key -in 'Root', 'Parent', 'Directory') {
                $lines += '{2}{0} = ''{1}''' -f $entry.Key, $entry.Value, $newIndent
                continue
            }
            $lines += '{2}{0} = {1}' -f $entry.Key, $this.ConvertValue($entry.Value, $newParents, $newDepth), $newIndent
        }

        $lines += "$oldIndent}"
        return $lines -join "`n"
    }

    [string] ConvertPSCustomObject([object]$Value, [object[]]$Parents, [int]$Depth) {
        if ($Value -in $Parents) { return "'$($Value.GetType().FullName) (recursed)'" }
        if ($this.Depth -gt 0 -and $Depth -ge $this.Depth) { return "'$($Value.GetType().FullName)'" }
        
        $newParents = @($Parents) + $Value

        $hash = [ordered]@{}
        foreach ($property in $Value.PSObject.Properties) {
            $hash[$property.Name] = $property.Value
        }
        return $this.ConvertHashtable($hash, $newParents, $Depth)
    }

    [string] Convert([object]$Value) {
        if ($Value -is [System.Collections.IDictionary]) {
            return $this.ConvertHashtable($Value, @(), 0)
        }
        if ($Value -isnot [PSCustomObject]) {
            return $this.ConvertValue($Value, @(), 0)
        }
        return $this.ConvertPSCustomObject($Value, @(), 0)
    }
}