Functions/GenXdev.Coding.Templating/Add-ArrayTemplate.ps1

################################################################################
<#
.SYNOPSIS
Formats a collection of objects using a template string with property
placeholders, producing a single formatted string.

.DESCRIPTION
Iterates over a collection of objects and applies a template string with
{PropertyName} or {PropertyName:format} placeholders to each item. The
formatted results are joined with a configurable delimiter. Supports
indentation, trailing newlines, and trailing delimiters for flexible output
formatting.

.LICENSE
Copyright (C) 2026 René Vaessen / GenXdev

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

.PARAMETER InputObject
The collection of objects to format. Each object's properties can be
referenced in the template string using {PropertyName} syntax.

.PARAMETER Template
The template string with {PropertyName} or {PropertyName:format}
placeholders that will be filled with values from each input object.

.PARAMETER Delimiter
The delimiter string to insert between formatted items. Default is a newline.

.PARAMETER Indentation
The number of indentation levels (4 spaces per level) to apply to each
formatted item.

.PARAMETER NewLine
When specified, appends a trailing newline after each formatted item.

.PARAMETER EndDelimiter
When specified, adds the delimiter at the end after the final item when the
array is not empty.

.EXAMPLE
@(
    @{ Name = "Alice"; Age = 30 }
    @{ Name = "Bob"; Age = 25 }
) | Add-ArrayTemplate -Template "{Name} is {Age} years old"

Name: Alice
Age: 30

Name: Bob
Age: 25

Formats an array of hashtables with property placeholders.

.EXAMPLE
$items | FormatArray -Template " - {Name}" -Delimiter ", " -Indentation 1

Uses the FormatArray alias with indentation and a custom delimiter.
#>

function Add-ArrayTemplate {

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [Alias('FormatArray')]
    [OutputType([string])]

    param(
        ###########################################################################
        [Parameter(
            Mandatory = $true,
            Position = 0,
            ValueFromPipeline = $true,
            HelpMessage = "The collection of objects to format"
        )]
        [System.Collections.IEnumerable] $InputObject,
        ###########################################################################
        [Parameter(
            Mandatory = $true,
            Position = 1,
            HelpMessage = (
                "The template string with {PropertyName} or " +
                "{PropertyName:format} placeholders"
            )
        )]
        [string] $Template,
        ###########################################################################
        [Parameter(
            Mandatory = $false,
            Position = 2,
            HelpMessage = (
                "The delimiter string to insert between formatted items"
            )
        )]
        [string] $Delimiter = "`n",
        ###########################################################################
        [Parameter(
            Mandatory = $false,
            Position = 3,
            HelpMessage = (
                "The number of indentation levels (4 spaces per level) to apply"
            )
        )]
        [int] $Indentation = 0,
        ###########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Appends a trailing delimiter after the final item"
        )]
        [switch] $NewLine = $false,
        ###########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Add delimiter at end when array not empty"
        )]
        [switch] $EndDelimiter = $false
        ###########################################################################
    )

    begin {

        # calculate the indentation string based on the specified level
        $indent = ' ' * $Indentation

        # create a string builder for efficient string concatenation
        $sb = [System.Text.StringBuilder]::new()

        # track whether this is the first item to avoid leading delimiter
        $first = $true
    }

    process {

        # iterate through each object in the input collection
        foreach ($item in $InputObject) {
            if ($null -eq $item) { continue }

            # add delimiter before all items except the first one
            if (-not $first) {
                $null = $sb.Append($Delimiter)
                if ($NewLine) {
                    $null = $sb.Append("`r`n$indent")
                }
            }

            # mark that we've processed the first item
            $first = $false

            # If the item is a scalar (not an object with properties), use an empty hashtable
            if ($item -is [PSObject]) {
                $item = $item.PSObject.BaseObject
            }
            if ($item -is [string] -or $item -is [int] -or $item -is [double] -or $item -is [bool] -or $item -is [char] -or $item -is [datetime]) {
            }
            elseif ($item -is [hashtable]) {
                $hash = $item
            }
            else {
                $hash = $item | GenXdev\ConvertTo-Hashtable
            }

            # Expand template inside an isolated scope
            $expanded = & {
                param($template, $hash, $item)

                if ($item -is [PSObject]) {
                    $item = $item.PSObject.BaseObject
                }
                if ($item -is [string] -or $item -is [int] -or $item -is [double] -or $item -is [bool] -or $item -is [char] -or $item -is [datetime]) {

                    $template = $template.Replace('$PSItem', '$item').Replace('$_', '$item')
                }
                else {

                    foreach ($entry in $hash.GetEnumerator()) {

                        Microsoft.PowerShell.Utility\Set-Variable -Name $entry.Key -Value $entry.Value -ErrorAction SilentlyContinue
                    }
                }

                $ExecutionContext.InvokeCommand.ExpandString($template)
            } $template $hash $item

            # Add delimiter between items (but not after last)
            $null = $sb.Append($expanded)
        }
    }

    end {

        # append trailing delimiter if requested and output is not empty
        if ((-not $first) -and ($endDelimiter)) {

            $null = $sb.Append($Delimiter)
            if ($NewLine) {
                $null = $sb.Append("`r`n$indent")
            }
        }

        # return the fully formatted string
        return $sb.ToString()
    }
}
################################################################################