Json.psm1

[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
    'PSAvoidAssignmentToAutomaticVariable', 'IsWindows',
    Justification = 'IsWindows doesnt exist in PS5.1'
)]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
    'PSUseDeclaredVarsMoreThanAssignments', 'IsWindows',
    Justification = 'IsWindows doesnt exist in PS5.1'
)]
[CmdletBinding()]
param()
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath)
$script:PSModuleInfo = Import-PowerShellDataFile -Path "$PSScriptRoot\$baseName.psd1"
$script:PSModuleInfo | Format-List | Out-String -Stream | ForEach-Object { Write-Debug $_ }
$scriptName = $script:PSModuleInfo.Name
Write-Debug "[$scriptName] - Importing module"

if ($PSEdition -eq 'Desktop') {
    $IsWindows = $true
}

#region [functions] - [public]
Write-Debug "[$scriptName] - [functions] - [public] - Processing folder"
#region [functions] - [public] - [Export-Json]
Write-Debug "[$scriptName] - [functions] - [public] - [Export-Json] - Importing"
function Export-Json {
    <#
        .SYNOPSIS
        Exports JSON data to a file.

        .DESCRIPTION
        Converts PowerShell objects to JSON format and writes them to one or more files.
        Supports various formatting options including indentation types, sizes, and compact output.
        Can accept both PowerShell objects and JSON strings as input.

        .EXAMPLE
        Export-Json -InputObject $myObject -Path 'output.json'

        Exports a PowerShell object to output.json with default formatting.

        .EXAMPLE
        Export-Json -InputObject $data -Path 'config.json' -IndentationType Spaces -IndentationSize 2

        Exports data to config.json with 2-space indentation.

        .EXAMPLE
        Export-Json -JsonString $jsonText -Path 'data.json' -Compact

        Exports a JSON string to data.json in compact format.

        .EXAMPLE
        $objects | Export-Json -Path 'output.json'

        Exports multiple objects to the same file via pipeline (last object overwrites).

        .EXAMPLE
        Export-Json -InputObject $config -Path 'settings.json' -IndentationType Tabs -Force

        Exports configuration to settings.json with tab indentation, overwriting if it exists.

        .LINK
        https://psmodule.io/Json/Functions/Export-Json/
    #>


    [CmdletBinding(DefaultParameterSetName = 'FromObject', SupportsShouldProcess)]
    param (
        # PowerShell object to convert and export as JSON.
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'FromObject')]
        [PSObject]$InputObject,

        # JSON string to export to file.
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'FromString')]
        [string]$JsonString,

        # The path to the output JSON file.
        [Parameter(Mandatory)]
        [string]$Path,

        # Produce compact (minified) output.
        [Parameter()]
        [switch]$Compact,

        # Indentation type: 'Spaces' or 'Tabs'.
        [Parameter()]
        [ValidateSet('Spaces', 'Tabs')]
        [string]$IndentationType = 'Spaces',

        # Number of spaces or tabs per indentation level. Only used if not compacting.
        [Parameter()]
        [UInt16]$IndentationSize = 2,

        # The maximum depth to serialize nested objects.
        [Parameter()]
        [int]$Depth = 2,

        # Overwrite existing files without prompting.
        [Parameter()]
        [switch]$Force,

        # Text encoding for the output file.
        [Parameter()]
        [ValidateSet('ASCII', 'BigEndianUnicode', 'BigEndianUTF32', 'OEM', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF8NoBOM', 'UTF32')]
        [string]$Encoding = 'UTF8NoBOM'
    )

    begin {
    }

    process {
        try {
            # Determine the input object
            $objectToExport = if ($PSCmdlet.ParameterSetName -eq 'FromString') {
                $JsonString | ConvertFrom-Json -Depth $Depth -ErrorAction Stop
            } else {
                $InputObject
            }

            # Generate the file path
            $outputPath = $Path

            # Resolve the path for consistent operations and error messages
            if (Test-Path -Path $outputPath) {
                $resolvedPath = Resolve-Path -Path $outputPath
            } else {
                # For non-existing files, resolve the parent directory and combine with filename
                $parentPath = Split-Path -Path $outputPath -Parent
                $fileName = Split-Path -Path $outputPath -Leaf
                if ($parentPath -and (Test-Path -Path $parentPath)) {
                    $resolvedParent = Resolve-Path -Path $parentPath
                    $resolvedPath = Join-Path -Path $resolvedParent -ChildPath $fileName
                } else {
                    # If parent doesn't exist either, use the original path as-is for error messages
                    $resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($outputPath)
                }
            }

            # Check if file exists and handle accordingly
            if ((Test-Path -Path $resolvedPath -PathType Leaf) -and -not $Force) {
                if ($PSCmdlet.ShouldProcess($resolvedPath, "Overwrite existing file")) {
                    # Continue with export
                } else {
                    # Only error if not WhatIf - WhatIf should just show what would happen
                    if (-not $WhatIfPreference) {
                        Write-Error "File already exists: $resolvedPath. Use -Force to overwrite."
                    }
                    return
                }
            }

            # Create directory if it doesn't exist
            $directory = Split-Path -Path $resolvedPath -Parent
            if ($directory -and -not (Test-Path -Path $directory -PathType Container)) {
                Write-Verbose "Creating directory: $directory"
                $null = New-Item -Path $directory -ItemType Directory -Force
            }

            # Format the JSON
            if ($Compact) {
                $formattedJson = $objectToExport | ConvertTo-Json -Depth $Depth -Compress
            } else {
                # Use Format-Json for consistent formatting
                $formattedJson = Format-Json -InputObject $objectToExport -IndentationType $IndentationType -IndentationSize $IndentationSize
            }

            # Write to file
            if ($PSCmdlet.ShouldProcess($resolvedPath, "Export JSON")) {
                Write-Verbose "Exporting JSON to: $resolvedPath"

                $writeParams = @{
                    Path     = $resolvedPath
                    Value    = $formattedJson
                    Encoding = $Encoding
                }

                # Only use Force for Set-Content if user explicitly requested it
                if ($Force) {
                    $writeParams['Force'] = $true
                }

                Set-Content @writeParams -ErrorAction Stop

                # Output file info object
                Get-Item -Path $resolvedPath | Add-Member -MemberType NoteProperty -Name 'JsonExported' -Value $true -PassThru
            }
        } catch [System.ArgumentException] {
            Write-Error "Invalid JSON format: $_"
        } catch [System.IO.DirectoryNotFoundException] {
            Write-Error "Directory not found or could not be created: $directory"
        } catch [System.UnauthorizedAccessException] {
            Write-Error "Access denied: $resolvedPath"
        } catch {
            Write-Error "Failed to export JSON to '$resolvedPath': $_"
        }
    }
}
Write-Debug "[$scriptName] - [functions] - [public] - [Export-Json] - Done"
#endregion [functions] - [public] - [Export-Json]
#region [functions] - [public] - [Format-Json]
Write-Debug "[$scriptName] - [functions] - [public] - [Format-Json] - Importing"
function Format-Json {
    <#
        .SYNOPSIS
        Formats a JSON string or PowerShell object.

        .DESCRIPTION
        Converts raw JSON strings or PowerShell objects into formatted JSON. Supports
        pretty-printing with configurable indentation or compact output.

        .EXAMPLE
        Format-Json -JsonString '{"a":1,"b":{"c":2}}' -IndentationType Spaces -IndentationSize 2

        .EXAMPLE
        $obj = @{ user = 'Marius'; roles = @('admin','dev') }
        Format-Json -InputObject $obj -IndentationType Tabs -IndentationSize 1

        .EXAMPLE
        Format-Json -JsonString '{"a":1,"b":{"c":2}}' -Compact

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


    [CmdletBinding(DefaultParameterSetName = 'FromString')]
    param (
        # JSON string to format.
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'FromString')]
        [string]$JsonString,

        # PowerShell object to convert and format as JSON.
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'FromObject')]
        [PSObject]$InputObject,

        # Produce compact (minified) output.
        [Parameter(ParameterSetName = 'FromString')]
        [Parameter(ParameterSetName = 'FromObject')]
        [switch]$Compact,

        # Indentation type: 'Spaces' or 'Tabs'.
        [Parameter(ParameterSetName = 'FromString')]
        [Parameter(ParameterSetName = 'FromObject')]
        [ValidateSet('Spaces', 'Tabs')]
        [string]$IndentationType = 'Spaces',

        # Number of spaces or tabs per indentation level. Only used if not compacting.
        [Parameter(ParameterSetName = 'FromString')]
        [Parameter(ParameterSetName = 'FromObject')]
        [UInt16]$IndentationSize = 2
    )

    process {
        try {
            $inputObject = if ($PSCmdlet.ParameterSetName -eq 'FromString') {
                $JsonString | ConvertFrom-Json -ErrorAction Stop
            } else {
                $InputObject
            }

            $json = $inputObject | ConvertTo-Json -Depth 100 -Compress:$Compact

            if ($Compact) {
                return $json
            }

            $indentUnit = switch ($IndentationType) {
                'Tabs' { "`t" }
                'Spaces' { ' ' * $IndentationSize }
            }

            $lines = $json -split "`n"
            $level = 0
            $result = foreach ($line in $lines) {
                $trimmed = $line.Trim()
                if ($trimmed -match '^[}\]]') {
                    $level = [Math]::Max(0, $level - 1)
                }
                $indent = $indentUnit * $level
                $indentedLine = "$indent$trimmed"
                # Check if the line ends with an opening bracket ('[' or '{') and is not a closing bracket ('}' or ']') or a comma.
                # This ensures that the indentation level is increased only for lines that introduce a new block.
                if ($trimmed -match '[{\[]$' -and $trimmed -notmatch '^[}\]],?$') {
                    $level++
                }
                $indentedLine
            }

            return ($result -join "`n")
        } catch {
            Write-Error "Failed to format JSON: $_"
        }
    }
}
Write-Debug "[$scriptName] - [functions] - [public] - [Format-Json] - Done"
#endregion [functions] - [public] - [Format-Json]
#region [functions] - [public] - [Import-Json]
Write-Debug "[$scriptName] - [functions] - [public] - [Import-Json] - Importing"
function Import-Json {
    <#
        .SYNOPSIS
        Imports JSON data from a file.

        .DESCRIPTION
        Reads JSON content from one or more files and converts it to PowerShell objects.
        Supports pipeline input for processing multiple files.

        .EXAMPLE
        Import-Json -Path 'config.json'

        Imports JSON data from config.json file.

        .EXAMPLE
        Import-Json -Path 'data/*.json'

        Imports JSON data from all .json files in the data directory.

        .EXAMPLE
        'settings.json', 'users.json' | Import-Json

        Imports JSON data from multiple files via pipeline.

        .EXAMPLE
        Import-Json -Path 'complex.json' -Depth 50

        Imports JSON data with a custom maximum depth of 50 levels.

        .LINK
        https://psmodule.io/Json/Functions/Import-Json/
    #>


    [CmdletBinding()]
    param (
        # The path to the JSON file to import. Supports wildcards and multiple paths. Can be provided via pipeline.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('FullName')]
        [string[]]$Path,

        # The maximum depth to expand nested objects. Uses ConvertFrom-Json default if not specified.
        [Parameter()]
        [int]$Depth
    )

    process {
        foreach ($filePath in $Path) {
            try {
                # Resolve wildcards and relative paths
                $resolvedPaths = Resolve-Path -Path $filePath -ErrorAction Stop

                foreach ($resolvedPath in $resolvedPaths) {
                    Write-Verbose "Processing file: $($resolvedPath.Path)"

                    # Test if the file exists and is a file (not directory)
                    if (-not (Test-Path -Path $resolvedPath.Path -PathType Leaf)) {
                        Write-Error "File not found or is not a file: $($resolvedPath.Path)"
                        continue
                    }

                    # Read file content
                    $jsonContent = Get-Content -Path $resolvedPath.Path -Raw -ErrorAction Stop

                    # Check if file is empty
                    if ([string]::IsNullOrWhiteSpace($jsonContent)) {
                        Write-Warning "File is empty or contains only whitespace: $($resolvedPath.Path)"
                        continue
                    }

                    # Convert JSON to PowerShell object
                    if ($PSBoundParameters.ContainsKey('Depth')) {
                        $jsonObject = $jsonContent | ConvertFrom-Json -Depth $Depth -ErrorAction Stop
                    } else {
                        $jsonObject = $jsonContent | ConvertFrom-Json -ErrorAction Stop
                    }

                    # Add file path information as a note property for reference
                    if ($jsonObject -is [PSCustomObject]) {
                        Add-Member -InputObject $jsonObject -MemberType NoteProperty -Name '_SourceFile' -Value $resolvedPath.Path -Force
                    }

                    # Output the object
                    $jsonObject
                }
            } catch [System.Management.Automation.ItemNotFoundException] {
                Write-Error "Path not found: $filePath"
            } catch [System.ArgumentException] {
                Write-Error "Invalid JSON format in file: $filePath. $_"
            } catch {
                Write-Error "Failed to import JSON from file '$filePath': $_"
            }
        }
    }
}
Write-Debug "[$scriptName] - [functions] - [public] - [Import-Json] - Done"
#endregion [functions] - [public] - [Import-Json]
Write-Debug "[$scriptName] - [functions] - [public] - Done"
#endregion [functions] - [public]

#region Member exporter
$exports = @{
    Alias    = '*'
    Cmdlet   = ''
    Function = @(
        'Export-Json'
        'Format-Json'
        'Import-Json'
    )
    Variable = ''
}
Export-ModuleMember @exports
#endregion Member exporter