From c178a7391edbe38a992e0a54d3a5fb1724e5309d Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Jul 2026 16:06:57 +0200 Subject: [PATCH 01/13] Implement Toml module with full TOML 1.0.0 support - Add ConvertFrom-Toml, ConvertTo-Toml, Import-Toml, Export-Toml public commands - Add TomlDocument class with Data (OrderedDictionary) and FilePath properties - Add private helpers: ConvertFrom-TomlDateTime, ConvertFrom-TomlynTable, ConvertFrom-TomlynValue, ConvertTo-TomlynArray, ConvertTo-TomlynTable, ConvertTo-TomlynValue - Back with Tomlyn v2.0.0 (.NET TOML library) via src/assemblies/Tomlyn.dll - Enforce TOML 1.0.0 duplicate key and table redefinition rules via SyntaxParser.ParseStrict before deserializing - Map all 8 TOML types to PowerShell types (string, long, double, bool, DateTimeOffset, DateTime, TimeSpan, OrderedDictionary/object[]) - Add 116 Pester tests covering spec compliance, round-trips, file I/O, error handling, and all TOML 1.0.0 type categories - Add build.ps1 for local module assembly and test running - Add tests/bootstrap.ps1 for test module import - Add tests/data/ with 9 TOML fixture files - Update README.md with type mapping table, command reference, usage examples - Update examples/General.ps1 with comprehensive usage examples - Remove template placeholder files (LsonLib.dll, format/type XMLs, sample modules/scripts/variables) - Fix PSScriptAnalyzer warnings: UTF-8 BOM, indentation consistency PSScriptAnalyzer: 0 errors, 0 warnings Tests: 116/116 pass Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .gitignore | 1 + README.md | 101 ++++- build.ps1 | 106 +++++ examples/General.ps1 | 69 ++- src/assemblies/LsonLib.dll | Bin 43520 -> 0 bytes src/assemblies/Tomlyn.dll | Bin 0 -> 325120 bytes src/classes/public/TomlDocument.ps1 | 35 ++ src/formats/CultureInfo.Format.ps1xml | 37 -- src/formats/Mygciview.Format.ps1xml | 65 --- .../private/Toml/ConvertFrom-TomlDateTime.ps1 | 59 +++ .../private/Toml/ConvertFrom-TomlynTable.ps1 | 43 ++ .../private/Toml/ConvertFrom-TomlynValue.ps1 | 74 +++ .../private/Toml/ConvertTo-TomlynArray.ps1 | 41 ++ .../private/Toml/ConvertTo-TomlynTable.ps1 | 55 +++ .../private/Toml/ConvertTo-TomlynValue.ps1 | 128 ++++++ src/functions/public/ConvertFrom-Toml.ps1 | 24 - .../public/Toml/ConvertFrom-Toml.ps1 | 101 +++++ src/functions/public/Toml/ConvertTo-Toml.ps1 | 97 ++++ src/functions/public/Toml/Export-Toml.ps1 | 72 +++ src/functions/public/Toml/Import-Toml.ps1 | 55 +++ src/init/initializer.ps1 | 10 +- src/modules/OtherPSModule.psm1 | 19 - src/scripts/loader.ps1 | 3 - src/types/DirectoryInfo.Types.ps1xml | 21 - src/types/FileInfo.Types.ps1xml | 14 - src/variables/private/PrivateVariables.ps1 | 47 -- src/variables/public/Moons.ps1 | 6 - src/variables/public/Planets.ps1 | 20 - src/variables/public/SolarSystems.ps1 | 17 - tests/ConvertFrom-Toml.Tests.ps1 | 420 ++++++++++++++++++ tests/ConvertTo-Toml.Tests.ps1 | 212 +++++++++ tests/Export-Toml.Tests.ps1 | 160 +++++++ tests/Import-Toml.Tests.ps1 | 114 +++++ tests/Toml.Tests.ps1 | 12 +- tests/bootstrap.ps1 | 17 + tests/data/array-of-tables.toml | 21 + tests/data/arrays.toml | 11 + tests/data/booleans.toml | 3 + tests/data/datetimes.toml | 6 + tests/data/floats.toml | 9 + tests/data/full-example.toml | 22 + tests/data/integers.toml | 9 + tests/data/strings.toml | 12 + tests/data/tables.toml | 9 + 44 files changed, 2065 insertions(+), 292 deletions(-) create mode 100644 build.ps1 delete mode 100644 src/assemblies/LsonLib.dll create mode 100644 src/assemblies/Tomlyn.dll create mode 100644 src/classes/public/TomlDocument.ps1 delete mode 100644 src/formats/CultureInfo.Format.ps1xml delete mode 100644 src/formats/Mygciview.Format.ps1xml create mode 100644 src/functions/private/Toml/ConvertFrom-TomlDateTime.ps1 create mode 100644 src/functions/private/Toml/ConvertFrom-TomlynTable.ps1 create mode 100644 src/functions/private/Toml/ConvertFrom-TomlynValue.ps1 create mode 100644 src/functions/private/Toml/ConvertTo-TomlynArray.ps1 create mode 100644 src/functions/private/Toml/ConvertTo-TomlynTable.ps1 create mode 100644 src/functions/private/Toml/ConvertTo-TomlynValue.ps1 delete mode 100644 src/functions/public/ConvertFrom-Toml.ps1 create mode 100644 src/functions/public/Toml/ConvertFrom-Toml.ps1 create mode 100644 src/functions/public/Toml/ConvertTo-Toml.ps1 create mode 100644 src/functions/public/Toml/Export-Toml.ps1 create mode 100644 src/functions/public/Toml/Import-Toml.ps1 delete mode 100644 src/modules/OtherPSModule.psm1 delete mode 100644 src/scripts/loader.ps1 delete mode 100644 src/types/DirectoryInfo.Types.ps1xml delete mode 100644 src/types/FileInfo.Types.ps1xml delete mode 100644 src/variables/private/PrivateVariables.ps1 delete mode 100644 src/variables/public/Moons.ps1 delete mode 100644 src/variables/public/Planets.ps1 delete mode 100644 src/variables/public/SolarSystems.ps1 create mode 100644 tests/ConvertFrom-Toml.Tests.ps1 create mode 100644 tests/ConvertTo-Toml.Tests.ps1 create mode 100644 tests/Export-Toml.Tests.ps1 create mode 100644 tests/Import-Toml.Tests.ps1 create mode 100644 tests/bootstrap.ps1 create mode 100644 tests/data/array-of-tables.toml create mode 100644 tests/data/arrays.toml create mode 100644 tests/data/booleans.toml create mode 100644 tests/data/datetimes.toml create mode 100644 tests/data/floats.toml create mode 100644 tests/data/full-example.toml create mode 100644 tests/data/integers.toml create mode 100644 tests/data/strings.toml create mode 100644 tests/data/tables.toml diff --git a/.gitignore b/.gitignore index 456ca0f..4238184 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ # PSModule framework outputs folder outputs/* +output/ # .Net build output bin/ diff --git a/README.md b/README.md index 6319793..7104745 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# {{ NAME }} +# Toml -{{ DESCRIPTION }} +PowerShell module for reading and writing [TOML](https://toml.io) data, with full TOML 1.0.0 specification support. ## Prerequisites -This uses the following external resources: +- PowerShell 7.2 or later - The [PSModule framework](https://github.com/PSModule/Process-PSModule) for building, testing and publishing the module. ## Installation @@ -12,23 +12,104 @@ This uses the following external resources: To install the module from the PowerShell Gallery, you can use the following command: ```powershell -Install-PSResource -Name {{ NAME }} -Import-Module -Name {{ NAME }} +Install-PSResource -Name Toml +Import-Module -Name Toml ``` ## Usage -Here is a list of example that are typical use cases for the module. +### Parse a TOML string -### Example 1: Greet an entity +```powershell +$doc = ConvertFrom-Toml -InputObject @' +[database] +host = "localhost" +port = 5432 +enabled = true +'@ + +$doc.Data.database.host # "localhost" +$doc.Data.database.port # 5432 [long] +$doc.Data.database.enabled # $true +``` -Provide examples for typical commands that a user would like to do with the module. +### Import from a TOML file ```powershell -Greet-Entity -Name 'World' -Hello, World! +$doc = Import-Toml -Path './config.toml' +$doc.FilePath # absolute path to the file +$doc.Data # OrderedDictionary of all top-level keys ``` +### Serialize an object to TOML + +```powershell +$toml = ConvertTo-Toml -InputObject ([ordered]@{ + title = 'My App' + version = 1 + server = [ordered]@{ + host = 'localhost' + port = 8080 + } +}) +# title = "My App" +# version = 1 +# +# [server] +# host = "localhost" +# port = 8080 +``` + +### Export to a TOML file + +```powershell +$config = [ordered]@{ + name = 'example' + enabled = $true +} +Export-Toml -InputObject $config -Path './output.toml' +``` + +### Round-trip: file → modify → file + +```powershell +$doc = Import-Toml -Path './config.toml' +$doc.Data['version'] = 2 +Export-Toml -InputObject $doc -Path './config.toml' +``` + +## TOML type mapping + +| TOML type | PowerShell type | +|----------------------|----------------------------| +| String | `[string]` | +| Integer | `[long]` | +| Float | `[double]` | +| Boolean | `[bool]` | +| Offset date-time | `[System.DateTimeOffset]` | +| Local date-time | `[System.DateTime]` | +| Local date | `[System.DateTime]` | +| Local time | `[System.TimeSpan]` | +| Array | `[object[]]` | +| Table / Inline table | `[ordered]` hashtable | +| Array of tables | `[object[]]` of hashtables | + +## Commands + +| Command | Description | +|-------------------|------------------------------------------| +| `ConvertFrom-Toml` | Parse TOML text → `TomlDocument` | +| `ConvertTo-Toml` | Serialize object → TOML text | +| `Import-Toml` | Read TOML file → `TomlDocument` | +| `Export-Toml` | Write object or `TomlDocument` to file | + +## Implementation notes + +- Backed by [Tomlyn v2.0.0](https://github.com/xoofx/Tomlyn), a conformant .NET TOML 1.0.0 library. +- Duplicate keys and table redefinition are rejected per the TOML 1.0.0 spec. +- Files are written as UTF-8 without BOM. + + ### Example 2 Provide examples for typical commands that a user would like to do with the module. diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..b9572f7 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,106 @@ +#!/usr/bin/env pwsh +#Requires -Version 7.0 +<# + .SYNOPSIS + Builds the Toml module from source into ./output/Toml/. + + .DESCRIPTION + Assembles all source files (classes, init, private functions, public functions) + into a single psm1 and creates a manifest, so tests can be run locally without + the PSModule CI pipeline. +#> +[CmdletBinding()] +param( + # Version to stamp into the manifest. + [string] $ModuleVersion = '0.0.1' +) + +$ErrorActionPreference = 'Stop' +$moduleName = 'Toml' +$srcPath = Join-Path $PSScriptRoot 'src' +$outputPath = Join-Path $PSScriptRoot 'output' $moduleName + +# ── clean / create output directory ───────────────────────────────────────── +if (Test-Path $outputPath) { + Remove-Item $outputPath -Recurse -Force +} +$null = New-Item -ItemType Directory -Path $outputPath -Force + +# ── copy assemblies ────────────────────────────────────────────────────────── +$assembliesSrc = Join-Path $srcPath 'assemblies' +$assembliesDst = Join-Path $outputPath 'assemblies' +$null = New-Item -ItemType Directory -Path $assembliesDst -Force +Copy-Item -Path (Join-Path $assembliesSrc '*.dll') -Destination $assembliesDst -ErrorAction SilentlyContinue + +# ── build psm1 ─────────────────────────────────────────────────────────────── +$psm1Path = Join-Path $outputPath "$moduleName.psm1" +$sb = [System.Text.StringBuilder]::new() + +# header +$headerPath = Join-Path $srcPath 'header.ps1' +if (Test-Path $headerPath) { + $null = $sb.AppendLine((Get-Content $headerPath -Raw)) +} + +# init — emit a corrected version that uses the assembled module's own PSScriptRoot +# The source init uses '../assemblies/Tomlyn.dll' relative to src/init/. +# In the built module, assemblies live next to the psm1, so we rewrite the path. +$initContent = @' +$assemblyPath = Join-Path -Path $PSScriptRoot -ChildPath 'assemblies\Tomlyn.dll' +$resolvedPath = [System.IO.Path]::GetFullPath($assemblyPath) + +if (-not [System.AppDomain]::CurrentDomain.GetAssemblies().Where({ $_.GetName().Name -eq 'Tomlyn' })) { + [System.Reflection.Assembly]::LoadFrom($resolvedPath) | Out-Null + Write-Verbose "Loaded Tomlyn assembly from: $resolvedPath" +} +'@ +$null = $sb.AppendLine($initContent) + +# classes private then public +foreach ($visibility in @('private', 'public')) { + $classPath = Join-Path $srcPath "classes/$visibility" + foreach ($f in (Get-ChildItem $classPath -Filter '*.ps1' -Recurse -ErrorAction SilentlyContinue)) { + $null = $sb.AppendLine((Get-Content $f.FullName -Raw)) + } +} + +# private functions +foreach ($f in (Get-ChildItem (Join-Path $srcPath 'functions/private') -Filter '*.ps1' -Recurse -ErrorAction SilentlyContinue)) { + $null = $sb.AppendLine((Get-Content $f.FullName -Raw)) +} + +# public functions +$publicFunctions = [System.Collections.Generic.List[string]]::new() +foreach ($f in (Get-ChildItem (Join-Path $srcPath 'functions/public') -Filter '*.ps1' -Recurse -ErrorAction SilentlyContinue)) { + $null = $sb.AppendLine((Get-Content $f.FullName -Raw)) + $publicFunctions.Add([System.IO.Path]::GetFileNameWithoutExtension($f.Name)) +} + +# finally +$finallyPath = Join-Path $srcPath 'finally.ps1' +if (Test-Path $finallyPath) { + $null = $sb.AppendLine((Get-Content $finallyPath -Raw)) +} + +# Export-ModuleMember +$null = $sb.AppendLine("Export-ModuleMember -Function @($($publicFunctions | ForEach-Object { "'$_'" } | Join-String -Separator ', '))") + +[System.IO.File]::WriteAllText($psm1Path, $sb.ToString(), [System.Text.UTF8Encoding]::new($false)) + +# ── write manifest ──────────────────────────────────────────────────────────── +$psd1Path = Join-Path $outputPath "$moduleName.psd1" +$manifest = @{ + Path = $psd1Path + ModuleVersion = $ModuleVersion + RootModule = "$moduleName.psm1" + FunctionsToExport = $publicFunctions.ToArray() + PowerShellVersion = '7.2' + Description = 'PowerShell module for reading and writing TOML data.' + Author = 'PSModule' + CompanyName = 'PSModule' + GUID = '6f1b6f8d-1234-4321-abcd-ef0123456789' +} +New-ModuleManifest @manifest + +Write-Host "Built $moduleName $ModuleVersion -> $outputPath" +Write-Host "Public functions: $($publicFunctions -join ', ')" diff --git a/examples/General.ps1 b/examples/General.ps1 index f29e88d..d9b484c 100644 --- a/examples/General.ps1 +++ b/examples/General.ps1 @@ -1,15 +1,76 @@ <# .SYNOPSIS - This is a general example of how to use the Toml module. + General usage examples for the Toml module. #> # Import the module Import-Module -Name 'Toml' -# Convert a TOML string to a PowerShell object -$toml = @' +# ── Parse TOML string ────────────────────────────────────────────────────── +$doc = ConvertFrom-Toml -InputObject @' [database] host = "localhost" port = 5432 +enabled = true +tags = ["production", "primary"] + +[database.credentials] +user = "admin" +'@ + +$doc.Data.database.host # "localhost" +$doc.Data.database.port # 5432 +$doc.Data.database.enabled # True +$doc.Data.database.tags # @("production", "primary") +$doc.Data.database.credentials.user # "admin" + +# ── Serialize to TOML ────────────────────────────────────────────────────── +$toml = ConvertTo-Toml -InputObject ([ordered]@{ + title = 'My Application' + version = 2 + debug = $false + server = [ordered]@{ + host = '0.0.0.0' + port = 8080 + } + features = @('auth', 'logging', 'metrics') +}) +Write-Host $toml + +# ── Import from file ─────────────────────────────────────────────────────── +# $doc = Import-Toml -Path './config.toml' +# $doc.FilePath # absolute path to the source file +# $doc.Data # [ordered] hashtable of all top-level keys + +# ── Export to file ───────────────────────────────────────────────────────── +# Export-Toml -InputObject ([ordered]@{ key = 'value' }) -Path './config.toml' + +# ── Round-trip: file → edit → file ──────────────────────────────────────── +# $doc = Import-Toml -Path './config.toml' +# $doc.Data['version'] = 3 +# Export-Toml -InputObject $doc -Path './config.toml' + +# ── Pipeline usage ───────────────────────────────────────────────────────── +# '[server] +# host = "localhost"' | ConvertFrom-Toml | ConvertTo-Toml + +# ── All TOML scalar types ────────────────────────────────────────────────── +$types = ConvertFrom-Toml -InputObject @' +a_string = "hello" +an_integer = 42 +a_float = 3.14 +a_bool = true +an_offset_dt = 1979-05-27T07:32:00Z +a_local_dt = 1979-05-27T07:32:00 +a_local_date = 1979-05-27 +a_local_time = 07:32:00 '@ -ConvertFrom-Toml -InputObject $toml + +$types.Data.a_string # [string] +$types.Data.an_integer # [long] +$types.Data.a_float # [double] +$types.Data.a_bool # [bool] +$types.Data.an_offset_dt # [System.DateTimeOffset] +$types.Data.a_local_dt # [System.DateTime] (Kind=Unspecified) +$types.Data.a_local_date # [System.DateTime] (00:00:00) +$types.Data.a_local_time # [System.TimeSpan] diff --git a/src/assemblies/LsonLib.dll b/src/assemblies/LsonLib.dll deleted file mode 100644 index 36618070d5c9f5131ec66720aa0565c13e86d23f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43520 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 literal 0 HcmV?d00001 diff --git a/src/classes/public/TomlDocument.ps1 b/src/classes/public/TomlDocument.ps1 new file mode 100644 index 0000000..3a6c46a --- /dev/null +++ b/src/classes/public/TomlDocument.ps1 @@ -0,0 +1,35 @@ +# Represents a parsed TOML document. +# Exposes the root key-value data as an ordered dictionary and records the +# file path when the document was loaded from disk. +class TomlDocument { + # The root key-value pairs of the TOML document, preserving insertion order. + [System.Collections.Specialized.OrderedDictionary] $Data + + # The absolute path to the source file, if loaded with Import-Toml. + # $null when the document was created from a string. + [string] $FilePath + + TomlDocument() { + $this.Data = [System.Collections.Specialized.OrderedDictionary]::new( + [System.StringComparer]::Ordinal + ) + } + + TomlDocument([System.Collections.Specialized.OrderedDictionary] $data) { + $this.Data = $data + } + + # Returns true when the document contains the given top-level key. + [bool] ContainsKey([string] $key) { + return $this.Data.Contains($key) + } + + # Returns the number of top-level keys. + [int] GetCount() { + return $this.Data.Count + } + + [string] ToString() { + return "TomlDocument[$($this.Data.Count) key(s)]" + } +} diff --git a/src/formats/CultureInfo.Format.ps1xml b/src/formats/CultureInfo.Format.ps1xml deleted file mode 100644 index a715e08..0000000 --- a/src/formats/CultureInfo.Format.ps1xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - System.Globalization.CultureInfo - - System.Globalization.CultureInfo - - - - - 16 - - - 16 - - - - - - - - LCID - - - Name - - - DisplayName - - - - - - - - diff --git a/src/formats/Mygciview.Format.ps1xml b/src/formats/Mygciview.Format.ps1xml deleted file mode 100644 index 4c972c2..0000000 --- a/src/formats/Mygciview.Format.ps1xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - mygciview - - System.IO.DirectoryInfo - System.IO.FileInfo - - - PSParentPath - - - - - - 7 - Left - - - - 26 - Right - - - - 26 - Right - - - - 14 - Right - - - - Left - - - - - - - - ModeWithoutHardLink - - - LastWriteTime - - - CreationTime - - - Length - - - Name - - - - - - - - diff --git a/src/functions/private/Toml/ConvertFrom-TomlDateTime.ps1 b/src/functions/private/Toml/ConvertFrom-TomlDateTime.ps1 new file mode 100644 index 0000000..eb9be73 --- /dev/null +++ b/src/functions/private/Toml/ConvertFrom-TomlDateTime.ps1 @@ -0,0 +1,59 @@ +function ConvertFrom-TomlDateTime { + <# + .SYNOPSIS + Converts a Tomlyn TomlDateTime to the appropriate PowerShell date/time type. + + .DESCRIPTION + Maps each TOML date/time variant to the most natural PowerShell type: + - OffsetDateTimeByZ / OffsetDateTimeByNumber -> [System.DateTimeOffset] + - LocalDateTime -> [System.DateTime] (Unspecified Kind) + - LocalDate -> [System.DateTime] (date only, 00:00:00) + - LocalTime -> [System.TimeSpan] (time of day) + + .EXAMPLE + ConvertFrom-TomlDateTime -TomlDt $tomlDateTimeValue + + Returns a [DateTimeOffset], [DateTime], or [TimeSpan] depending on kind. + + .INPUTS + [Tomlyn.TomlDateTime] + + .OUTPUTS + [object] — [System.DateTimeOffset], [System.DateTime], or [System.TimeSpan] + + .NOTES + Internal helper. Not exported. No pipeline input. + #> + [OutputType([System.DateTimeOffset], [System.DateTime], [System.TimeSpan])] + [CmdletBinding()] + param( + # The Tomlyn TomlDateTime to convert. + [Parameter(Mandatory)] + [Tomlyn.TomlDateTime] $TomlDt + ) + + switch ($TomlDt.Kind) { + ([Tomlyn.TomlDateTimeKind]::OffsetDateTimeByZ) { + return $TomlDt.DateTime + } + ([Tomlyn.TomlDateTimeKind]::OffsetDateTimeByNumber) { + return $TomlDt.DateTime + } + ([Tomlyn.TomlDateTimeKind]::LocalDateTime) { + # Strip offset — keep year/month/day/hour/minute/second as-is + $utc = $TomlDt.DateTime.DateTime + return [System.DateTime]::SpecifyKind($utc, [System.DateTimeKind]::Unspecified) + } + ([Tomlyn.TomlDateTimeKind]::LocalDate) { + $utc = $TomlDt.DateTime.Date + return [System.DateTime]::SpecifyKind($utc, [System.DateTimeKind]::Unspecified) + } + ([Tomlyn.TomlDateTimeKind]::LocalTime) { + return $TomlDt.DateTime.TimeOfDay + } + default { + # Fallback: return DateTimeOffset + return $TomlDt.DateTime + } + } +} diff --git a/src/functions/private/Toml/ConvertFrom-TomlynTable.ps1 b/src/functions/private/Toml/ConvertFrom-TomlynTable.ps1 new file mode 100644 index 0000000..dccaa19 --- /dev/null +++ b/src/functions/private/Toml/ConvertFrom-TomlynTable.ps1 @@ -0,0 +1,43 @@ +function ConvertFrom-TomlynTable { + <# + .SYNOPSIS + Converts a Tomlyn TomlTable to an [ordered] hashtable. + + .DESCRIPTION + Iterates every key-value pair in the Tomlyn model TomlTable and + calls ConvertFrom-TomlynValue recursively, building an + [ordered] hashtable that preserves TOML key order. + + .EXAMPLE + $table = [Tomlyn.TomlSerializer]::Deserialize[Tomlyn.Model.TomlTable]($toml, $opts) + ConvertFrom-TomlynTable -Table $table + + Returns an [ordered] hashtable of the table's contents. + + .INPUTS + [Tomlyn.Model.TomlTable] + + .OUTPUTS + [System.Collections.Specialized.OrderedDictionary] + + .NOTES + Internal helper. Not exported. No pipeline input. + #> + [OutputType([System.Collections.Specialized.OrderedDictionary])] + [CmdletBinding()] + param( + # The Tomlyn TomlTable to convert. + [Parameter(Mandatory)] + [Tomlyn.Model.TomlTable] $Table + ) + + $dict = [System.Collections.Specialized.OrderedDictionary]::new( + [System.StringComparer]::Ordinal + ) + + foreach ($kv in $Table) { + $dict[$kv.Key] = ConvertFrom-TomlynValue -Value $kv.Value + } + + return $dict +} diff --git a/src/functions/private/Toml/ConvertFrom-TomlynValue.ps1 b/src/functions/private/Toml/ConvertFrom-TomlynValue.ps1 new file mode 100644 index 0000000..5f76b42 --- /dev/null +++ b/src/functions/private/Toml/ConvertFrom-TomlynValue.ps1 @@ -0,0 +1,74 @@ +function ConvertFrom-TomlynValue { + <# + .SYNOPSIS + Converts a Tomlyn model value to a native PowerShell value. + + .DESCRIPTION + Recursively maps the Tomlyn object model (TomlTable, TomlTableArray, + TomlArray, TomlDateTime, and scalars) to corresponding PowerShell types: + - Tomlyn.Model.TomlTable -> [System.Collections.Specialized.OrderedDictionary] + - Tomlyn.Model.TomlTableArray -> [object[]] of [ordered] + - Tomlyn.Model.TomlArray -> [object[]] + - Tomlyn.TomlDateTime -> [DateTimeOffset], [datetime], or [timespan] + - string, bool, long, double -> their PowerShell equivalents + + .EXAMPLE + $table = [Tomlyn.TomlSerializer]::Deserialize[Tomlyn.Model.TomlTable]($toml, $opts) + ConvertFrom-TomlynValue -Value $table + + Returns an [ordered] hashtable representing the TOML table. + + .INPUTS + [object] — any Tomlyn model value. + + .OUTPUTS + [object] + + .NOTES + Internal helper. Not exported. No pipeline input. + #> + [OutputType( + [System.Collections.Specialized.OrderedDictionary], [object[]], + [string], [long], [double], [bool], + [System.DateTimeOffset], [System.DateTime], [System.TimeSpan] + )] + [CmdletBinding()] + param( + # The Tomlyn model value to convert. + [Parameter(Mandatory)] + [AllowNull()] + [object] $Value + ) + + if ($null -eq $Value) { + return $null + } + + if ($Value -is [Tomlyn.Model.TomlTableArray]) { + # Array of tables: [[key]] sections + $list = [System.Collections.Generic.List[object]]::new() + foreach ($tableItem in $Value) { + $list.Add((ConvertFrom-TomlynTable -Table $tableItem)) + } + return , $list.ToArray() + } + + if ($Value -is [Tomlyn.Model.TomlTable]) { + return ConvertFrom-TomlynTable -Table $Value + } + + if ($Value -is [Tomlyn.Model.TomlArray]) { + $list = [System.Collections.Generic.List[object]]::new() + foreach ($item in $Value) { + $list.Add((ConvertFrom-TomlynValue -Value $item)) + } + return , $list.ToArray() + } + + if ($Value -is [Tomlyn.TomlDateTime]) { + return ConvertFrom-TomlDateTime -TomlDt $Value + } + + # Scalar: string, bool, long, double — already a native .NET type + return $Value +} diff --git a/src/functions/private/Toml/ConvertTo-TomlynArray.ps1 b/src/functions/private/Toml/ConvertTo-TomlynArray.ps1 new file mode 100644 index 0000000..68931f2 --- /dev/null +++ b/src/functions/private/Toml/ConvertTo-TomlynArray.ps1 @@ -0,0 +1,41 @@ +function ConvertTo-TomlynArray { + <# + .SYNOPSIS + Converts a PowerShell enumerable to a Tomlyn TomlArray. + + .DESCRIPTION + Iterates each element of the input collection and calls ConvertTo-TomlynValue + recursively. Strings are enumerated character-by-character by default in + PowerShell, so strings are handled explicitly before the IEnumerable path. + + .EXAMPLE + ConvertTo-TomlynArray -Value @(1, 2, 3) + + Returns a [Tomlyn.Model.TomlArray] with three integer elements. + + .INPUTS + [System.Collections.IEnumerable] + + .OUTPUTS + [Tomlyn.Model.TomlArray] + + .NOTES + Internal helper. Not exported. No pipeline input. + #> + [OutputType([Tomlyn.Model.TomlArray])] + [CmdletBinding()] + param( + # The collection to convert. + [Parameter(Mandatory)] + [System.Collections.IEnumerable] $Value + ) + + $array = [Tomlyn.Model.TomlArray]::new() + + foreach ($item in $Value) { + $array.Add((ConvertTo-TomlynValue -Value $item)) + } + + # Wrap to prevent PowerShell from enumerating TomlArray (IEnumerable) + return , $array +} diff --git a/src/functions/private/Toml/ConvertTo-TomlynTable.ps1 b/src/functions/private/Toml/ConvertTo-TomlynTable.ps1 new file mode 100644 index 0000000..c49d7ff --- /dev/null +++ b/src/functions/private/Toml/ConvertTo-TomlynTable.ps1 @@ -0,0 +1,55 @@ +function ConvertTo-TomlynTable { + <# + .SYNOPSIS + Converts a PowerShell dictionary or PSCustomObject to a Tomlyn TomlTable. + + .DESCRIPTION + Iterates the properties/keys of the input and calls ConvertTo-TomlynValue + recursively for each value. Accepts [hashtable], [ordered], + [System.Collections.Specialized.OrderedDictionary], and [PSCustomObject]. + + .EXAMPLE + ConvertTo-TomlynTable -Value @{ host = 'localhost'; port = 5432 } + + Returns a [Tomlyn.Model.TomlTable] with two entries. + + .INPUTS + [object] + + .OUTPUTS + [Tomlyn.Model.TomlTable] + + .NOTES + Internal helper. Not exported. No pipeline input. + #> + [OutputType([Tomlyn.Model.TomlTable])] + [CmdletBinding()] + param( + # The dictionary or PSCustomObject to convert. + [Parameter(Mandatory)] + [object] $Value + ) + + $table = [Tomlyn.Model.TomlTable]::new() + + if ($Value -is [System.Management.Automation.PSCustomObject]) { + foreach ($prop in $Value.PSObject.Properties) { + if ($prop.MemberType -notin 'NoteProperty', 'ScriptProperty', 'Property') { + continue + } + $table[$prop.Name] = ConvertTo-TomlynValue -Value $prop.Value + } + } elseif ($Value -is [System.Collections.IDictionary]) { + foreach ($key in $Value.Keys) { + $table[$key.ToString()] = ConvertTo-TomlynValue -Value $Value[$key] + } + } else { + throw [System.InvalidOperationException]::new( + "Cannot convert '$($Value.GetType().FullName)' to a TOML table. " + + 'Provide a [hashtable], [ordered], or [PSCustomObject].' + ) + } + + # Wrap in array to prevent PowerShell from enumerating TomlTable (which implements IEnumerable) + return , $table +} diff --git a/src/functions/private/Toml/ConvertTo-TomlynValue.ps1 b/src/functions/private/Toml/ConvertTo-TomlynValue.ps1 new file mode 100644 index 0000000..b89ccf2 --- /dev/null +++ b/src/functions/private/Toml/ConvertTo-TomlynValue.ps1 @@ -0,0 +1,128 @@ +function ConvertTo-TomlynValue { + <# + .SYNOPSIS + Converts a PowerShell value to the appropriate Tomlyn model object. + + .DESCRIPTION + Maps PowerShell types to Tomlyn model objects used by TomlSerializer: + - [System.DateTimeOffset] -> Tomlyn.TomlDateTime (OffsetDateTimeByNumber) + - [System.DateTime] (date-only = midnight) -> Tomlyn.TomlDateTime (LocalDate) + - [System.DateTime] (has time component) -> Tomlyn.TomlDateTime (LocalDateTime) + - [System.TimeSpan] -> Tomlyn.TomlDateTime (LocalTime) + - [hashtable] / [OrderedDictionary] / [PSCustomObject] / [PSObject] -> TomlTable + - [array] / [List] / [IEnumerable] -> TomlArray + - [bool] -> bool + - [int16/32/64] / [byte] / [sbyte] -> long + - [single/double] -> double + - [string] -> string + - everything else -> string via ToString() + + .EXAMPLE + ConvertTo-TomlynValue -Value 42 + Returns [long] 42. + + .EXAMPLE + ConvertTo-TomlynValue -Value @{ key = 'val' } + Returns a [Tomlyn.Model.TomlTable]. + + .INPUTS + [object] + + .OUTPUTS + [object] + + .NOTES + Internal helper. Not exported. No pipeline input. + #> + [OutputType([object])] + [CmdletBinding()] + param( + # The PowerShell value to convert. + [Parameter(Mandatory)] + [AllowNull()] + [object] $Value + ) + + if ($null -eq $Value) { + throw [System.ArgumentNullException]::new( + 'Value', + 'TOML does not support null values. Remove the key or supply a valid value.' + ) + } + + # Unwrap PSObject wrapper + if ($Value -is [System.Management.Automation.PSObject] -and + $Value -isnot [System.Management.Automation.PSCustomObject]) { + $Value = $Value.BaseObject + } + + if ($Value -is [System.DateTimeOffset]) { + return [Tomlyn.TomlDateTime]::new( + $Value, + 0, + [Tomlyn.TomlDateTimeKind]::OffsetDateTimeByNumber + ) + } + + if ($Value -is [System.DateTime]) { + if ($Value.TimeOfDay -eq [System.TimeSpan]::Zero) { + # Date-only: use the (year, month, day) constructor for LocalDate + return [Tomlyn.TomlDateTime]::new($Value.Year, $Value.Month, $Value.Day) + } else { + # Date + time: use (DateTime) constructor for LocalDateTime + return [Tomlyn.TomlDateTime]::new( + [System.DateTime]::SpecifyKind($Value, [System.DateTimeKind]::Unspecified) + ) + } + } + + if ($Value -is [System.TimeSpan]) { + # LocalTime — build a DateTimeOffset on epoch with the time component + $dto = [System.DateTimeOffset]::new( + 1970, 1, 1, + $Value.Hours, $Value.Minutes, $Value.Seconds, + [System.TimeSpan]::Zero + ) + return [Tomlyn.TomlDateTime]::new($dto, 0, [Tomlyn.TomlDateTimeKind]::LocalTime) + } + + if ($Value -is [bool]) { + return $Value + } + + if ($Value -is [System.Int16] -or + $Value -is [System.Int32] -or + $Value -is [System.Int64] -or + $Value -is [System.Byte] -or + $Value -is [System.SByte] -or + $Value -is [System.UInt16] -or + $Value -is [System.UInt32] -or + $Value -is [System.UInt64]) { + return [long]$Value + } + + if ($Value -is [System.Single] -or $Value -is [System.Double]) { + return [double]$Value + } + + if ($Value -is [string]) { + return $Value + } + + if ($Value -is [System.Collections.IDictionary] -or + $Value -is [System.Management.Automation.PSCustomObject]) { + $subTable = ConvertTo-TomlynTable -Value $Value + # Use , to prevent PowerShell from iterating the TomlTable (IEnumerable) in the pipeline + return , $subTable + } + + # Array and list types + if ($Value -is [System.Collections.IEnumerable]) { + $subArray = ConvertTo-TomlynArray -Value $Value + # Use , to prevent PowerShell from iterating the TomlArray (IEnumerable) in the pipeline + return , $subArray + } + + # Fallback: stringify + return $Value.ToString() +} diff --git a/src/functions/public/ConvertFrom-Toml.ps1 b/src/functions/public/ConvertFrom-Toml.ps1 deleted file mode 100644 index 7e67845..0000000 --- a/src/functions/public/ConvertFrom-Toml.ps1 +++ /dev/null @@ -1,24 +0,0 @@ -function ConvertFrom-Toml { - <# - .SYNOPSIS - Converts a TOML string to a PowerShell object. - - .DESCRIPTION - Converts a TOML formatted string into a PowerShell hashtable or object. - - .EXAMPLE - ConvertFrom-Toml -InputObject '[database] - host = "localhost" - port = 5432' - - Converts a TOML string to a PowerShell object. - #> - [CmdletBinding()] - param ( - # The TOML string to convert. - [Parameter(Mandatory)] - [string] $InputObject - ) - $null = $InputObject - throw [System.NotImplementedException] 'ConvertFrom-Toml is not yet implemented.' -} diff --git a/src/functions/public/Toml/ConvertFrom-Toml.ps1 b/src/functions/public/Toml/ConvertFrom-Toml.ps1 new file mode 100644 index 0000000..da96dfb --- /dev/null +++ b/src/functions/public/Toml/ConvertFrom-Toml.ps1 @@ -0,0 +1,101 @@ +function ConvertFrom-Toml { + <# + .SYNOPSIS + Converts a TOML string to a TomlDocument object. + + .DESCRIPTION + Parses a TOML-formatted string using the Tomlyn library and returns a + TomlDocument whose Data property contains an ordered dictionary of the + document's key-value pairs. TOML tables become nested ordered dictionaries, + arrays become object arrays, and TOML scalar types are mapped to PowerShell + types as follows: + + | TOML type | PowerShell type | + |---------------------|--------------------------| + | String | [string] | + | Integer | [long] | + | Float | [double] | + | Boolean | [bool] | + | Offset date-time | [System.DateTimeOffset] | + | Local date-time | [System.DateTime] | + | Local date | [System.DateTime] | + | Local time | [System.TimeSpan] | + | Array | [object[]] | + | Table / Inline table| [ordered] hashtable | + + Full TOML 1.0.0 specification is supported. + + .EXAMPLE + $doc = ConvertFrom-Toml -InputObject '[database] + host = "localhost" + port = 5432' + + $doc.Data.database.host # returns 'localhost' + $doc.Data.database.port # returns 5432 + + Parses a simple TOML document with a table and two keys. + + .EXAMPLE + $toml = Get-Content 'config.toml' -Raw + $config = ConvertFrom-Toml -InputObject $toml + $config.Data.server.timeout + + Reads a TOML file and accesses a nested value. + + .INPUTS + [string] + + .OUTPUTS + [TomlDocument] + + .NOTES + Throws [Tomlyn.TomlException] or [System.InvalidOperationException] on + invalid TOML input. Error messages include line and column information + from the Tomlyn parser. + #> + [OutputType([TomlDocument])] + [CmdletBinding()] + param( + # The TOML-formatted string to parse. Must be valid TOML 1.0.0. + [Parameter(Mandatory, ValueFromPipeline)] + [ValidateNotNullOrEmpty()] + [string] $InputObject + ) + + process { + $opts = [Tomlyn.TomlSerializerOptions]::new() + $tomlTable = $null + + try { + # SyntaxParser.ParseStrict validates the full document and throws a + # detailed exception on duplicate keys or table redefinition, which + # TomlSerializer.Deserialize does not enforce by itself. + $null = [Tomlyn.Parsing.SyntaxParser]::ParseStrict($InputObject, $opts, $null, $true) + } catch { + throw [System.InvalidOperationException]::new( + "Failed to parse TOML: $($_.Exception.Message)", + $_.Exception + ) + } + + try { + $tomlTable = [Tomlyn.TomlSerializer]::Deserialize[Tomlyn.Model.TomlTable]( + $InputObject, + $opts + ) + } catch [Tomlyn.TomlException] { + throw [System.InvalidOperationException]::new( + "Failed to parse TOML: $($_.Exception.Message)", + $_.Exception + ) + } catch { + throw [System.InvalidOperationException]::new( + "Unexpected error parsing TOML: $($_.Exception.Message)", + $_.Exception + ) + } + + $data = ConvertFrom-TomlynTable -Table $tomlTable + [TomlDocument]::new($data) + } +} diff --git a/src/functions/public/Toml/ConvertTo-Toml.ps1 b/src/functions/public/Toml/ConvertTo-Toml.ps1 new file mode 100644 index 0000000..4739b4e --- /dev/null +++ b/src/functions/public/Toml/ConvertTo-Toml.ps1 @@ -0,0 +1,97 @@ +function ConvertTo-Toml { + <# + .SYNOPSIS + Converts a PowerShell object graph to a TOML string. + + .DESCRIPTION + Serializes a [TomlDocument], [hashtable], [ordered] dictionary, or + [PSCustomObject] into a TOML-formatted string using the Tomlyn library. + + Accepted input types and their TOML encoding: + - [string] -> TOML String + - [bool] -> TOML Boolean (true / false) + - [int16/32/64] / [byte] -> TOML Integer + - [single] / [double] -> TOML Float + - [System.DateTimeOffset] -> TOML Offset date-time + - [System.DateTime] (no time component) -> TOML Local date + - [System.DateTime] (has time) -> TOML Local date-time + - [System.TimeSpan] -> TOML Local time + - [hashtable] / [ordered] / [PSCustomObject] -> TOML Table + - [array] / [List] / IEnumerable -> TOML Array + - [TomlDocument] -> TOML document from .Data + + Null values are not supported by TOML and cause a terminating error. + + .EXAMPLE + ConvertTo-Toml -InputObject ([ordered]@{ + title = 'Config' + server = [ordered]@{ host = 'localhost'; port = 8080 } + }) + + Produces: + title = "Config" + [server] + host = "localhost" + port = 8080 + + .EXAMPLE + $doc = ConvertFrom-Toml -InputObject $tomlString + ConvertTo-Toml -InputObject $doc + + Round-trips a parsed TOML document back to a TOML string. + + .INPUTS + [object] + + .OUTPUTS + [string] + + .NOTES + The output format follows Tomlyn's default serialization, which produces + valid TOML 1.0.0. Key order is preserved when the input is an [ordered] + dictionary or [TomlDocument]. + #> + [OutputType([string])] + [CmdletBinding()] + param( + # The object to serialize to TOML. Accepts TomlDocument, hashtable, + # ordered dictionary, or PSCustomObject. + [Parameter(Mandatory, ValueFromPipeline)] + [ValidateNotNull()] + [object] $InputObject + ) + + process { + # Unwrap TomlDocument to its data dict + $source = if ($InputObject -is [TomlDocument]) { + $InputObject.Data + } else { + $InputObject + } + + $tomlTable = $null + try { + $tomlTable = ConvertTo-TomlynTable -Value $source + } catch [System.ArgumentNullException] { + throw [System.InvalidOperationException]::new( + "Cannot serialize object: $($_.Exception.Message)", + $_.Exception + ) + } catch { + throw [System.InvalidOperationException]::new( + "Failed to build TOML model: $($_.Exception.Message)", + $_.Exception + ) + } + + $opts = [Tomlyn.TomlSerializerOptions]::new() + try { + [Tomlyn.TomlSerializer]::Serialize[Tomlyn.Model.TomlTable]($tomlTable, $opts) + } catch { + throw [System.InvalidOperationException]::new( + "Failed to serialize TOML: $($_.Exception.Message)", + $_.Exception + ) + } + } +} diff --git a/src/functions/public/Toml/Export-Toml.ps1 b/src/functions/public/Toml/Export-Toml.ps1 new file mode 100644 index 0000000..b4ce236 --- /dev/null +++ b/src/functions/public/Toml/Export-Toml.ps1 @@ -0,0 +1,72 @@ +function Export-Toml { + <# + .SYNOPSIS + Serializes an object graph to a TOML file. + + .DESCRIPTION + Converts the input object to a TOML string using ConvertTo-Toml and + writes it to the specified file path. Intermediate directories are + created when they do not exist. + + The file is written in UTF-8 encoding without a BOM, which is the + recommended encoding for TOML files. + + .EXAMPLE + $config = [ordered]@{ + title = 'My App' + server = [ordered]@{ host = 'localhost'; port = 8080 } + } + Export-Toml -InputObject $config -Path 'config.toml' + + Writes a TOML file with a string key and a nested table. + + .EXAMPLE + Import-Toml 'input.toml' | Export-Toml -Path 'output.toml' + + Round-trips a TOML file: parse then re-serialize. + + .INPUTS + [object] — pipeline input supported. + + .OUTPUTS + [void] — nothing is written to the output stream. + + .NOTES + Throws when: + - the object graph contains null values (TOML has no null) + - the object graph contains types that cannot be serialized to TOML + - the file cannot be created or written + #> + [CmdletBinding(SupportsShouldProcess)] + param( + # The object to serialize. Accepts TomlDocument, hashtable, + # ordered dictionary, or PSCustomObject. + [Parameter(Mandatory, ValueFromPipeline)] + [ValidateNotNull()] + [object] $InputObject, + + # Destination file path. Created (including parent directories) if absent. + [Parameter(Mandatory, Position = 0)] + [ValidateNotNullOrEmpty()] + [string] $Path + ) + + process { + $tomlString = ConvertTo-Toml -InputObject $InputObject + + $resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) + $parent = [System.IO.Path]::GetDirectoryName($resolvedPath) + + if (-not [System.IO.Directory]::Exists($parent)) { + $null = [System.IO.Directory]::CreateDirectory($parent) + } + + if ($PSCmdlet.ShouldProcess($resolvedPath, 'Write TOML file')) { + [System.IO.File]::WriteAllText( + $resolvedPath, + $tomlString, + [System.Text.UTF8Encoding]::new($false) # UTF-8 without BOM + ) + } + } +} diff --git a/src/functions/public/Toml/Import-Toml.ps1 b/src/functions/public/Toml/Import-Toml.ps1 new file mode 100644 index 0000000..51037e0 --- /dev/null +++ b/src/functions/public/Toml/Import-Toml.ps1 @@ -0,0 +1,55 @@ +function Import-Toml { + <# + .SYNOPSIS + Imports and parses a TOML file into a TomlDocument. + + .DESCRIPTION + Reads the content of a TOML file from disk and parses it using + ConvertFrom-Toml. The returned TomlDocument includes the resolved + absolute file path in its FilePath property. + + UTF-8 encoding (with or without BOM) is used when reading the file. + + .EXAMPLE + $config = Import-Toml -Path 'config.toml' + $config.Data.database.host + + Reads config.toml and returns a TomlDocument. The nested value is + accessed via the Data property. + + .EXAMPLE + Import-Toml -Path 'settings.toml' | ForEach-Object { $_.Data } + + Pipes the document and inspects the data dictionary. + + .INPUTS + [string] — pipeline input supported for the Path parameter. + + .OUTPUTS + [TomlDocument] + + .NOTES + Throws when: + - the file does not exist + - the file cannot be read + - the file content is not valid TOML 1.0.0 + #> + [OutputType([TomlDocument])] + [CmdletBinding()] + param( + # Path to the TOML file to import. Accepts relative and absolute paths. + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [ValidateNotNullOrEmpty()] + [string] $Path + ) + + process { + $resolvedPath = Resolve-Path -Path $Path -ErrorAction Stop + + $content = [System.IO.File]::ReadAllText($resolvedPath.ProviderPath) + + $doc = ConvertFrom-Toml -InputObject $content + $doc.FilePath = $resolvedPath.ProviderPath + $doc + } +} diff --git a/src/init/initializer.ps1 b/src/init/initializer.ps1 index 28396fb..c3cf3f5 100644 --- a/src/init/initializer.ps1 +++ b/src/init/initializer.ps1 @@ -1,3 +1,7 @@ -Write-Verbose '-------------------------------' -Write-Verbose '--- THIS IS AN INITIALIZER ---' -Write-Verbose '-------------------------------' +$assemblyPath = Join-Path -Path $PSScriptRoot -ChildPath '..\assemblies\Tomlyn.dll' +$resolvedPath = [System.IO.Path]::GetFullPath($assemblyPath) + +if (-not [System.AppDomain]::CurrentDomain.GetAssemblies().Where({ $_.GetName().Name -eq 'Tomlyn' })) { + [System.Reflection.Assembly]::LoadFrom($resolvedPath) | Out-Null + Write-Verbose "Loaded Tomlyn assembly from: $resolvedPath" +} diff --git a/src/modules/OtherPSModule.psm1 b/src/modules/OtherPSModule.psm1 deleted file mode 100644 index 5d6af8e..0000000 --- a/src/modules/OtherPSModule.psm1 +++ /dev/null @@ -1,19 +0,0 @@ -function Get-OtherPSModule { - <# - .SYNOPSIS - Performs tests on a module. - - .DESCRIPTION - A longer description of the function. - - .EXAMPLE - Get-OtherPSModule -Name 'World' - #> - [CmdletBinding()] - param( - # Name of the person to greet. - [Parameter(Mandatory)] - [string] $Name - ) - Write-Output "Hello, $Name!" -} diff --git a/src/scripts/loader.ps1 b/src/scripts/loader.ps1 deleted file mode 100644 index 973735a..0000000 --- a/src/scripts/loader.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -Write-Verbose '-------------------------' -Write-Verbose '--- THIS IS A LOADER ---' -Write-Verbose '-------------------------' diff --git a/src/types/DirectoryInfo.Types.ps1xml b/src/types/DirectoryInfo.Types.ps1xml deleted file mode 100644 index aef538b..0000000 --- a/src/types/DirectoryInfo.Types.ps1xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - System.IO.FileInfo - - - Status - Success - - - - - System.IO.DirectoryInfo - - - Status - Success - - - - diff --git a/src/types/FileInfo.Types.ps1xml b/src/types/FileInfo.Types.ps1xml deleted file mode 100644 index 4cfaf6b..0000000 --- a/src/types/FileInfo.Types.ps1xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - System.IO.FileInfo - - - Age - - ((Get-Date) - ($this.CreationTime)).Days - - - - - diff --git a/src/variables/private/PrivateVariables.ps1 b/src/variables/private/PrivateVariables.ps1 deleted file mode 100644 index f1fc2c3..0000000 --- a/src/variables/private/PrivateVariables.ps1 +++ /dev/null @@ -1,47 +0,0 @@ -$script:HabitablePlanets = @( - @{ - Name = 'Earth' - Mass = 5.97 - Diameter = 12756 - DayLength = 24.0 - }, - @{ - Name = 'Mars' - Mass = 0.642 - Diameter = 6792 - DayLength = 24.7 - }, - @{ - Name = 'Proxima Centauri b' - Mass = 1.17 - Diameter = 11449 - DayLength = 5.15 - }, - @{ - Name = 'Kepler-442b' - Mass = 2.34 - Diameter = 11349 - DayLength = 5.7 - }, - @{ - Name = 'Kepler-452b' - Mass = 5.0 - Diameter = 17340 - DayLength = 20.0 - } -) - -$script:InhabitedPlanets = @( - @{ - Name = 'Earth' - Mass = 5.97 - Diameter = 12756 - DayLength = 24.0 - }, - @{ - Name = 'Mars' - Mass = 0.642 - Diameter = 6792 - DayLength = 24.7 - } -) diff --git a/src/variables/public/Moons.ps1 b/src/variables/public/Moons.ps1 deleted file mode 100644 index dd0f33c..0000000 --- a/src/variables/public/Moons.ps1 +++ /dev/null @@ -1,6 +0,0 @@ -$script:Moons = @( - @{ - Planet = 'Earth' - Name = 'Moon' - } -) diff --git a/src/variables/public/Planets.ps1 b/src/variables/public/Planets.ps1 deleted file mode 100644 index 5927bc5..0000000 --- a/src/variables/public/Planets.ps1 +++ /dev/null @@ -1,20 +0,0 @@ -$script:Planets = @( - @{ - Name = 'Mercury' - Mass = 0.330 - Diameter = 4879 - DayLength = 4222.6 - }, - @{ - Name = 'Venus' - Mass = 4.87 - Diameter = 12104 - DayLength = 2802.0 - }, - @{ - Name = 'Earth' - Mass = 5.97 - Diameter = 12756 - DayLength = 24.0 - } -) diff --git a/src/variables/public/SolarSystems.ps1 b/src/variables/public/SolarSystems.ps1 deleted file mode 100644 index acbcedf..0000000 --- a/src/variables/public/SolarSystems.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -$script:SolarSystems = @( - @{ - Name = 'Solar System' - Planets = $script:Planets - Moons = $script:Moons - }, - @{ - Name = 'Alpha Centauri' - Planets = @() - Moons = @() - }, - @{ - Name = 'Sirius' - Planets = @() - Moons = @() - } -) diff --git a/tests/ConvertFrom-Toml.Tests.ps1 b/tests/ConvertFrom-Toml.Tests.ps1 new file mode 100644 index 0000000..c563543 --- /dev/null +++ b/tests/ConvertFrom-Toml.Tests.ps1 @@ -0,0 +1,420 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester tests' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester tests' +)] +[CmdletBinding()] +param() + +Describe 'Toml' { + BeforeAll { + . (Join-Path $PSScriptRoot 'bootstrap.ps1') + } + + Describe 'ConvertFrom-Toml' { + + Context 'ConvertFrom-Toml - return type' { + It 'ConvertFrom-Toml - returns a TomlDocument for a minimal document' { + $result = ConvertFrom-Toml -InputObject 'key = "value"' + $result.GetType().Name | Should -Be 'TomlDocument' + } + + It 'ConvertFrom-Toml - Data property is an OrderedDictionary' { + $result = ConvertFrom-Toml -InputObject 'key = "value"' + $result.Data | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] + } + + It 'ConvertFrom-Toml - FilePath is null when parsed from string' { + $result = ConvertFrom-Toml -InputObject 'key = "value"' + $result.FilePath | Should -BeNullOrEmpty + } + + It 'ConvertFrom-Toml - accepts pipeline input' { + $result = 'key = "value"' | ConvertFrom-Toml + $result.GetType().Name | Should -Be 'TomlDocument' + } + } + + Context 'ConvertFrom-Toml - strings' { + It 'ConvertFrom-Toml - parses basic string' { + $result = ConvertFrom-Toml -InputObject 'key = "hello"' + $result.Data['key'] | Should -Be 'hello' + } + + It 'ConvertFrom-Toml - parses literal string' { + $result = ConvertFrom-Toml -InputObject "key = 'C:\path'" + $result.Data['key'] | Should -Be 'C:\path' + } + + It 'ConvertFrom-Toml - parses basic string with escape sequences' { + $result = ConvertFrom-Toml -InputObject 'key = "tab\there"' + $result.Data['key'] | Should -Be "tab`there" + } + + It 'ConvertFrom-Toml - parses basic string with quoted escape' { + $result = ConvertFrom-Toml -InputObject 'key = "say \"hi\""' + $result.Data['key'] | Should -Be 'say "hi"' + } + + It 'ConvertFrom-Toml - parses Unicode escape sequence' { + $result = ConvertFrom-Toml -InputObject 'key = "\u03B1"' + $result.Data['key'] | Should -Be ([char]0x03B1).ToString() + } + + It 'ConvertFrom-Toml - parses multi-line basic string' { + $toml = "key = `"`"`"`nline one`nline two`n`"`"`"" + $result = ConvertFrom-Toml -InputObject $toml + $result.Data['key'] | Should -Match 'line one' + $result.Data['key'] | Should -Match 'line two' + } + + It 'ConvertFrom-Toml - parses multi-line literal string' { + $toml = "key = '''" + [System.Environment]::NewLine + "raw\nno escape" + [System.Environment]::NewLine + "'''" + $result = ConvertFrom-Toml -InputObject $toml + $result.Data['key'] | Should -Match 'raw\\nno escape' + } + + It 'ConvertFrom-Toml - parses empty string' { + $result = ConvertFrom-Toml -InputObject 'key = ""' + $result.Data['key'] | Should -Be '' + } + + It 'ConvertFrom-Toml - parses strings from file' { + $path = Join-Path $PSScriptRoot 'data\strings.toml' + $result = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) + $result.Data['literal'] | Should -Be 'C:\Users\nodejs\templates' + $result.Data['empty'] | Should -Be '' + } + } + + Context 'ConvertFrom-Toml - integers' { + It 'ConvertFrom-Toml - parses decimal integer' { + $result = ConvertFrom-Toml -InputObject 'n = 42' + $result.Data['n'] | Should -Be 42 + $result.Data['n'] | Should -BeOfType [long] + } + + It 'ConvertFrom-Toml - parses negative integer' { + $result = ConvertFrom-Toml -InputObject 'n = -17' + $result.Data['n'] | Should -Be -17 + } + + It 'ConvertFrom-Toml - parses zero' { + $result = ConvertFrom-Toml -InputObject 'n = 0' + $result.Data['n'] | Should -Be 0 + } + + It 'ConvertFrom-Toml - parses integer with underscore separator' { + $result = ConvertFrom-Toml -InputObject 'n = 1_000_000' + $result.Data['n'] | Should -Be 1000000 + } + + It 'ConvertFrom-Toml - parses hexadecimal integer' { + $result = ConvertFrom-Toml -InputObject 'n = 0xFF' + $result.Data['n'] | Should -Be 255 + } + + It 'ConvertFrom-Toml - parses octal integer' { + $result = ConvertFrom-Toml -InputObject 'n = 0o17' + $result.Data['n'] | Should -Be 15 + } + + It 'ConvertFrom-Toml - parses binary integer' { + $result = ConvertFrom-Toml -InputObject 'n = 0b1010' + $result.Data['n'] | Should -Be 10 + } + + It 'ConvertFrom-Toml - parses all integer forms from file' { + $path = Join-Path $PSScriptRoot 'data\integers.toml' + $result = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) + $result.Data['decimal'] | Should -Be 42 + $result.Data['negative'] | Should -Be -17 + $result.Data['hex'] | Should -Be 3735928559 # 0xDEADBEEF as unsigned long + $result.Data['octal'] | Should -Be 493 # 0o755 + $result.Data['binary'] | Should -Be 214 # 0b11010110 + } + } + + Context 'ConvertFrom-Toml - floats' { + It 'ConvertFrom-Toml - parses positive float' { + $result = ConvertFrom-Toml -InputObject 'n = 3.14' + $result.Data['n'] | Should -Be 3.14 + $result.Data['n'] | Should -BeOfType [double] + } + + It 'ConvertFrom-Toml - parses negative float' { + $result = ConvertFrom-Toml -InputObject 'n = -0.01' + $result.Data['n'] | Should -Be -0.01 + } + + It 'ConvertFrom-Toml - parses scientific notation' { + $result = ConvertFrom-Toml -InputObject 'n = 5e+22' + $result.Data['n'] | Should -Be 5e22 + } + + It 'ConvertFrom-Toml - parses inf' { + $result = ConvertFrom-Toml -InputObject 'n = inf' + $result.Data['n'] | Should -Be ([double]::PositiveInfinity) + } + + It 'ConvertFrom-Toml - parses -inf' { + $result = ConvertFrom-Toml -InputObject 'n = -inf' + $result.Data['n'] | Should -Be ([double]::NegativeInfinity) + } + + It 'ConvertFrom-Toml - parses nan' { + $result = ConvertFrom-Toml -InputObject 'n = nan' + [double]::IsNaN($result.Data['n']) | Should -BeTrue + } + + It 'ConvertFrom-Toml - parses floats from file' { + $path = Join-Path $PSScriptRoot 'data\floats.toml' + $result = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) + ([double]$result.Data['positive'] - 3.1415) | Should -BeLessThan 0.0001 + ([double]$result.Data['negative'] - (-0.01)) | Should -BeLessThan 0.0001 + } + } + + Context 'ConvertFrom-Toml - booleans' { + It 'ConvertFrom-Toml - parses true' { + $result = ConvertFrom-Toml -InputObject 'flag = true' + $result.Data['flag'] | Should -Be $true + $result.Data['flag'] | Should -BeOfType [bool] + } + + It 'ConvertFrom-Toml - parses false' { + $result = ConvertFrom-Toml -InputObject 'flag = false' + $result.Data['flag'] | Should -Be $false + } + } + + Context 'ConvertFrom-Toml - datetimes' { + It 'ConvertFrom-Toml - parses offset datetime with Z suffix as DateTimeOffset' { + $result = ConvertFrom-Toml -InputObject 'dt = 1979-05-27T07:32:00Z' + $result.Data['dt'] | Should -BeOfType [System.DateTimeOffset] + ([System.DateTimeOffset]$result.Data['dt']).Year | Should -Be 1979 + ([System.DateTimeOffset]$result.Data['dt']).Month | Should -Be 5 + ([System.DateTimeOffset]$result.Data['dt']).Day | Should -Be 27 + } + + It 'ConvertFrom-Toml - parses offset datetime with numeric offset as DateTimeOffset' { + $result = ConvertFrom-Toml -InputObject 'dt = 1979-05-27T07:32:00+05:30' + $result.Data['dt'] | Should -BeOfType [System.DateTimeOffset] + $offset = ([System.DateTimeOffset]$result.Data['dt']).Offset + $offset.Hours | Should -Be 5 + $offset.Minutes | Should -Be 30 + } + + It 'ConvertFrom-Toml - parses local datetime as DateTime with Unspecified kind' { + $result = ConvertFrom-Toml -InputObject 'dt = 1979-05-27T07:32:00' + $result.Data['dt'] | Should -BeOfType [System.DateTime] + ([System.DateTime]$result.Data['dt']).Kind | Should -Be ([System.DateTimeKind]::Unspecified) + ([System.DateTime]$result.Data['dt']).Year | Should -Be 1979 + } + + It 'ConvertFrom-Toml - parses local date as DateTime with zero time' { + $result = ConvertFrom-Toml -InputObject 'dt = 1979-05-27' + $result.Data['dt'] | Should -BeOfType [System.DateTime] + $dt = [System.DateTime]$result.Data['dt'] + $dt.Year | Should -Be 1979 + $dt.Month | Should -Be 5 + $dt.Day | Should -Be 27 + $dt.Hour | Should -Be 0 + $dt.Minute | Should -Be 0 + $dt.Second | Should -Be 0 + } + + It 'ConvertFrom-Toml - parses local time as TimeSpan' { + $result = ConvertFrom-Toml -InputObject 'tm = 07:32:00' + $result.Data['tm'] | Should -BeOfType [System.TimeSpan] + ([System.TimeSpan]$result.Data['tm']).Hours | Should -Be 7 + ([System.TimeSpan]$result.Data['tm']).Minutes | Should -Be 32 + } + + It 'ConvertFrom-Toml - parses all datetime types from file' { + $path = Join-Path $PSScriptRoot 'data\datetimes.toml' + $result = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) + $result.Data['offset_dt_z'] | Should -BeOfType [System.DateTimeOffset] + $result.Data['offset_dt_num'] | Should -BeOfType [System.DateTimeOffset] + $result.Data['local_dt'] | Should -BeOfType [System.DateTime] + $result.Data['local_date'] | Should -BeOfType [System.DateTime] + $result.Data['local_time'] | Should -BeOfType [System.TimeSpan] + } + } + + Context 'ConvertFrom-Toml - arrays' { + It 'ConvertFrom-Toml - parses integer array' { + $result = ConvertFrom-Toml -InputObject 'arr = [1, 2, 3]' + $result.Data['arr'] | Should -HaveCount 3 + $result.Data['arr'][0] | Should -Be 1 + $result.Data['arr'][2] | Should -Be 3 + } + + It 'ConvertFrom-Toml - parses string array' { + $result = ConvertFrom-Toml -InputObject 'arr = ["a", "b", "c"]' + $result.Data['arr'][0] | Should -Be 'a' + $result.Data['arr'][1] | Should -Be 'b' + } + + It 'ConvertFrom-Toml - parses empty array' { + $result = ConvertFrom-Toml -InputObject 'arr = []' + $result.Data['arr'] | Should -HaveCount 0 + } + + It 'ConvertFrom-Toml - parses nested array' { + $result = ConvertFrom-Toml -InputObject 'arr = [[1, 2], [3, 4]]' + $result.Data['arr'] | Should -HaveCount 2 + $result.Data['arr'][0] | Should -HaveCount 2 + $result.Data['arr'][0][1] | Should -Be 2 + } + + It 'ConvertFrom-Toml - parses multiline array' { + $toml = "arr = [`n 1,`n 2,`n 3,`n]" + $result = ConvertFrom-Toml -InputObject $toml + $result.Data['arr'] | Should -HaveCount 3 + } + + It 'ConvertFrom-Toml - parses arrays from file' { + $path = Join-Path $PSScriptRoot 'data\arrays.toml' + $result = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) + $result.Data['integers'] | Should -HaveCount 3 + $result.Data['strings'] | Should -HaveCount 3 + $result.Data['empty'] | Should -HaveCount 0 + $result.Data['nested'] | Should -HaveCount 2 + } + } + + Context 'ConvertFrom-Toml - tables' { + It 'ConvertFrom-Toml - parses standard table' { + $toml = "[server]`nhost = `"localhost`"" + $result = ConvertFrom-Toml -InputObject $toml + $result.Data['server'] | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] + $result.Data['server']['host'] | Should -Be 'localhost' + } + + It 'ConvertFrom-Toml - parses inline table' { + $result = ConvertFrom-Toml -InputObject 'point = { x = 1, y = 2 }' + $result.Data['point'] | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] + $result.Data['point']['x'] | Should -Be 1 + $result.Data['point']['y'] | Should -Be 2 + } + + It 'ConvertFrom-Toml - parses nested tables via dotted header' { + $toml = "[a.b.c]`nkey = `"deep`"" + $result = ConvertFrom-Toml -InputObject $toml + $result.Data['a']['b']['c']['key'] | Should -Be 'deep' + } + + It 'ConvertFrom-Toml - tables from file' { + $path = Join-Path $PSScriptRoot 'data\tables.toml' + $result = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) + $result.Data['simple']['key'] | Should -Be 'value' + $result.Data['dotted']['key']['works'] | Should -Be $true + $result.Data['inline_parent']['inline']['one'] | Should -Be 1 + } + } + + Context 'ConvertFrom-Toml - array of tables' { + It 'ConvertFrom-Toml - parses array of tables' { + $path = Join-Path $PSScriptRoot 'data\array-of-tables.toml' + $result = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) + $result.Data['products'] | Should -HaveCount 2 + $result.Data['products'][0]['name'] | Should -Be 'Hammer' + $result.Data['products'][1]['name'] | Should -Be 'Nail' + } + + It 'ConvertFrom-Toml - parses nested array of tables' { + $path = Join-Path $PSScriptRoot 'data\array-of-tables.toml' + $result = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) + $result.Data['fruits'][0]['varieties'] | Should -HaveCount 2 + $result.Data['fruits'][0]['varieties'][0]['name'] | Should -Be 'red delicious' + } + } + + Context 'ConvertFrom-Toml - keys' { + It 'ConvertFrom-Toml - parses bare key' { + $result = ConvertFrom-Toml -InputObject 'bare_key = "value"' + $result.Data['bare_key'] | Should -Be 'value' + } + + It 'ConvertFrom-Toml - parses quoted key' { + $result = ConvertFrom-Toml -InputObject '"quoted key" = "value"' + $result.Data['quoted key'] | Should -Be 'value' + } + + It 'ConvertFrom-Toml - parses dotted key' { + $result = ConvertFrom-Toml -InputObject 'a.b = "value"' + $result.Data['a']['b'] | Should -Be 'value' + } + + It 'ConvertFrom-Toml - preserves key order' { + $toml = "z = 1`ny = 2`nx = 3" + $result = ConvertFrom-Toml -InputObject $toml + $keys = $result.Data.Keys + $keys[0] | Should -Be 'z' + $keys[1] | Should -Be 'y' + $keys[2] | Should -Be 'x' + } + } + + Context 'ConvertFrom-Toml - comments and whitespace' { + It 'ConvertFrom-Toml - ignores inline comments' { + $result = ConvertFrom-Toml -InputObject 'key = "value" # this is a comment' + $result.Data['key'] | Should -Be 'value' + } + + It 'ConvertFrom-Toml - ignores full-line comments' { + $toml = "# comment`nkey = `"value`"" + $result = ConvertFrom-Toml -InputObject $toml + $result.Data['key'] | Should -Be 'value' + } + + It 'ConvertFrom-Toml - handles CRLF line endings' { + $toml = "a = 1`r`nb = 2" + $result = ConvertFrom-Toml -InputObject $toml + $result.Data['a'] | Should -Be 1 + $result.Data['b'] | Should -Be 2 + } + } + + Context 'ConvertFrom-Toml - full example file' { + It 'ConvertFrom-Toml - parses full example document' { + $path = Join-Path $PSScriptRoot 'data\full-example.toml' + $result = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) + $result.Data['title'] | Should -Be 'TOML Example' + $result.Data['owner']['name'] | Should -Be 'Tom Preston-Werner' + $result.Data['database']['enabled'] | Should -Be $true + $result.Data['database']['ports'] | Should -HaveCount 3 + $result.Data['servers']['alpha']['ip'] | Should -Be '10.0.0.1' + $result.Data['servers']['beta']['role'] | Should -Be 'backend' + } + } + + Context 'ConvertFrom-Toml - error handling' { + It 'ConvertFrom-Toml - throws on invalid TOML syntax' { + { ConvertFrom-Toml -InputObject 'invalid = = "bad"' } | Should -Throw + } + + It 'ConvertFrom-Toml - throws on duplicate key' { + $toml = "key = 1`nkey = 2" + { ConvertFrom-Toml -InputObject $toml } | Should -Throw + } + + It 'ConvertFrom-Toml - throws on missing value' { + { ConvertFrom-Toml -InputObject 'key =' } | Should -Throw + } + + It 'ConvertFrom-Toml - throws on empty string input' { + { ConvertFrom-Toml -InputObject '' } | Should -Throw + } + + It 'ConvertFrom-Toml - throws on redefining a table as a key' { + $toml = "[a]`nkey = 1`n[a]`nother = 2" + { ConvertFrom-Toml -InputObject $toml } | Should -Throw + } + } + } +} diff --git a/tests/ConvertTo-Toml.Tests.ps1 b/tests/ConvertTo-Toml.Tests.ps1 new file mode 100644 index 0000000..04d455b --- /dev/null +++ b/tests/ConvertTo-Toml.Tests.ps1 @@ -0,0 +1,212 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester tests' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester tests' +)] +[CmdletBinding()] +param() + +Describe 'Toml' { + BeforeAll { + . (Join-Path $PSScriptRoot 'bootstrap.ps1') + } + + Describe 'ConvertTo-Toml' { + + Context 'ConvertTo-Toml - return type' { + It 'ConvertTo-Toml - returns a string' { + $result = ConvertTo-Toml -InputObject ([ordered]@{ key = 'value' }) + $result | Should -BeOfType [string] + } + + It 'ConvertTo-Toml - result is non-empty for non-empty input' { + $result = ConvertTo-Toml -InputObject ([ordered]@{ key = 'value' }) + $result | Should -Not -BeNullOrEmpty + } + + It 'ConvertTo-Toml - accepts pipeline input' { + $result = [ordered]@{ key = 'value' } | ConvertTo-Toml + $result | Should -BeOfType [string] + } + } + + Context 'ConvertTo-Toml - string serialization' { + It 'ConvertTo-Toml - serializes a simple string key' { + $result = ConvertTo-Toml -InputObject ([ordered]@{ title = 'Hello' }) + $result | Should -Match 'title = "Hello"' + } + + It 'ConvertTo-Toml - output is valid TOML that round-trips' { + $original = [ordered]@{ key = 'value' } + $toml = ConvertTo-Toml -InputObject $original + $roundTrip = ConvertFrom-Toml -InputObject $toml + $roundTrip.Data['key'] | Should -Be 'value' + } + } + + Context 'ConvertTo-Toml - integer serialization' { + It 'ConvertTo-Toml - serializes integer' { + $result = ConvertTo-Toml -InputObject ([ordered]@{ n = [long]42 }) + $result | Should -Match 'n = 42' + } + + It 'ConvertTo-Toml - integer round-trips correctly' { + $original = [ordered]@{ n = [long]12345 } + $toml = ConvertTo-Toml -InputObject $original + $rt = ConvertFrom-Toml -InputObject $toml + $rt.Data['n'] | Should -Be 12345 + } + } + + Context 'ConvertTo-Toml - float serialization' { + It 'ConvertTo-Toml - serializes float' { + $result = ConvertTo-Toml -InputObject ([ordered]@{ pi = 3.14 }) + $result | Should -Match 'pi = ' + $result | Should -Match '3.14' + } + + It 'ConvertTo-Toml - float round-trips correctly' { + $original = [ordered]@{ n = 2.718 } + $toml = ConvertTo-Toml -InputObject $original + $rt = ConvertFrom-Toml -InputObject $toml + ([double]$rt.Data['n'] - 2.718) | Should -BeLessThan 0.001 + } + } + + Context 'ConvertTo-Toml - boolean serialization' { + It 'ConvertTo-Toml - serializes true' { + $result = ConvertTo-Toml -InputObject ([ordered]@{ flag = $true }) + $result | Should -Match 'flag = true' + } + + It 'ConvertTo-Toml - serializes false' { + $result = ConvertTo-Toml -InputObject ([ordered]@{ flag = $false }) + $result | Should -Match 'flag = false' + } + } + + Context 'ConvertTo-Toml - datetime serialization' { + It 'ConvertTo-Toml - serializes DateTimeOffset as offset datetime' { + $dto = [System.DateTimeOffset]::new(1979, 5, 27, 7, 32, 0, [System.TimeSpan]::Zero) + $result = ConvertTo-Toml -InputObject ([ordered]@{ dt = $dto }) + $result | Should -Match '1979-05-27' + } + + It 'ConvertTo-Toml - DateTimeOffset round-trips' { + $dto = [System.DateTimeOffset]::new(2024, 3, 15, 12, 0, 0, [System.TimeSpan]::FromHours(2)) + $original = [ordered]@{ dt = $dto } + $toml = ConvertTo-Toml -InputObject $original + $rt = ConvertFrom-Toml -InputObject $toml + $rt.Data['dt'] | Should -BeOfType [System.DateTimeOffset] + ([System.DateTimeOffset]$rt.Data['dt']).Year | Should -Be 2024 + } + + It 'ConvertTo-Toml - serializes DateTime with time as local datetime' { + $dt = [System.DateTime]::new(2024, 6, 1, 15, 30, 0, [System.DateTimeKind]::Unspecified) + $result = ConvertTo-Toml -InputObject ([ordered]@{ dt = $dt }) + $result | Should -Match '2024-06-01' + } + + It 'ConvertTo-Toml - serializes date-only DateTime as local date' { + $dt = [System.DateTime]::new(2024, 6, 1, 0, 0, 0, [System.DateTimeKind]::Unspecified) + $result = ConvertTo-Toml -InputObject ([ordered]@{ dt = $dt }) + $result | Should -Match '2024-06-01' + } + + It 'ConvertTo-Toml - serializes TimeSpan as local time' { + $ts = [System.TimeSpan]::new(0, 8, 30, 0) + $result = ConvertTo-Toml -InputObject ([ordered]@{ tm = $ts }) + $result | Should -Match '08:30:00' + } + } + + Context 'ConvertTo-Toml - nested table serialization' { + It 'ConvertTo-Toml - serializes nested ordered dict as TOML table' { + $obj = [ordered]@{ + server = [ordered]@{ host = 'localhost'; port = [long]8080 } + } + $result = ConvertTo-Toml -InputObject $obj + $result | Should -Match '\[server\]' + $result | Should -Match 'host = "localhost"' + } + + It 'ConvertTo-Toml - nested table round-trips' { + $original = [ordered]@{ + database = [ordered]@{ host = 'db.example.com'; port = [long]5432 } + } + $toml = ConvertTo-Toml -InputObject $original + $rt = ConvertFrom-Toml -InputObject $toml + $rt.Data['database']['host'] | Should -Be 'db.example.com' + $rt.Data['database']['port'] | Should -Be 5432 + } + } + + Context 'ConvertTo-Toml - array serialization' { + It 'ConvertTo-Toml - serializes array of integers' { + $obj = [ordered]@{ ports = @([long]80, [long]443, [long]8080) } + $result = ConvertTo-Toml -InputObject $obj + $result | Should -Match '80' + $result | Should -Match '443' + } + + It 'ConvertTo-Toml - integer array round-trips' { + $original = [ordered]@{ nums = @([long]1, [long]2, [long]3) } + $toml = ConvertTo-Toml -InputObject $original + $rt = ConvertFrom-Toml -InputObject $toml + $rt.Data['nums'] | Should -HaveCount 3 + $rt.Data['nums'][0] | Should -Be 1 + } + } + + Context 'ConvertTo-Toml - PSCustomObject input' { + It 'ConvertTo-Toml - serializes PSCustomObject' { + $obj = [PSCustomObject]@{ name = 'Alice'; age = [long]30 } + $result = ConvertTo-Toml -InputObject $obj + $result | Should -Match 'name = "Alice"' + $result | Should -Match 'age = 30' + } + } + + Context 'ConvertTo-Toml - TomlDocument input' { + It 'ConvertTo-Toml - accepts TomlDocument from ConvertFrom-Toml' { + $toml = "title = `"Test`"`n[section]`nvalue = 42" + $doc = ConvertFrom-Toml -InputObject $toml + $result = ConvertTo-Toml -InputObject $doc + $result | Should -Match 'title = "Test"' + $result | Should -Match '\[section\]' + } + } + + Context 'ConvertTo-Toml - round-trip' { + It 'ConvertTo-Toml - full example round-trips losslessly for string/integer/boolean/float' { + $path = Join-Path $PSScriptRoot 'data\full-example.toml' + $original = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) + $toml = ConvertTo-Toml -InputObject $original + $rt = ConvertFrom-Toml -InputObject $toml + $rt.Data['title'] | Should -Be 'TOML Example' + $rt.Data['owner']['name'] | Should -Be 'Tom Preston-Werner' + $rt.Data['database']['enabled'] | Should -Be $true + $rt.Data['database']['ports'] | Should -HaveCount 3 + $rt.Data['servers']['alpha']['ip'] | Should -Be '10.0.0.1' + } + + It 'ConvertTo-Toml - array of integers round-trips' { + $path = Join-Path $PSScriptRoot 'data\arrays.toml' + $original = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) + $toml = ConvertTo-Toml -InputObject $original + $rt = ConvertFrom-Toml -InputObject $toml + $rt.Data['integers'] | Should -HaveCount 3 + $rt.Data['strings'] | Should -HaveCount 3 + } + } + + Context 'ConvertTo-Toml - error handling' { + It 'ConvertTo-Toml - throws on null input' { + { ConvertTo-Toml -InputObject $null } | Should -Throw + } + } + } +} diff --git a/tests/Export-Toml.Tests.ps1 b/tests/Export-Toml.Tests.ps1 new file mode 100644 index 0000000..9cd06aa --- /dev/null +++ b/tests/Export-Toml.Tests.ps1 @@ -0,0 +1,160 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester tests' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester tests' +)] +[CmdletBinding()] +param() + +Describe 'Toml' { + BeforeAll { + . (Join-Path $PSScriptRoot 'bootstrap.ps1') + } + + Describe 'Export-Toml' { + + Context 'Export-Toml - basic write' { + It 'Export-Toml - creates a file at the given path' { + $path = Join-Path $TestDrive 'output.toml' + Export-Toml -InputObject ([ordered]@{ key = 'value' }) -Path $path + Test-Path -Path $path | Should -BeTrue + } + + It 'Export-Toml - file content is valid TOML' { + $path = Join-Path $TestDrive 'output.toml' + Export-Toml -InputObject ([ordered]@{ title = 'Hello' }) -Path $path + $content = Get-Content -Path $path -Raw + $content | Should -Match 'title = "Hello"' + } + + It 'Export-Toml - returns nothing to the output stream' { + $path = Join-Path $TestDrive 'void.toml' + $result = Export-Toml -InputObject ([ordered]@{ k = 'v' }) -Path $path + $result | Should -BeNullOrEmpty + } + + It 'Export-Toml - creates parent directory when it does not exist' { + $path = Join-Path $TestDrive 'nested\subdir\output.toml' + Export-Toml -InputObject ([ordered]@{ k = 'v' }) -Path $path + Test-Path -Path $path | Should -BeTrue + } + } + + Context 'Export-Toml - round-trip via Import-Toml' { + It 'Export-Toml - round-trip preserves string values' { + $inputDoc = [ordered]@{ greeting = 'Hello World' } + $path = Join-Path $TestDrive 'rt-string.toml' + Export-Toml -InputObject $inputDoc -Path $path + $result = Import-Toml -Path $path + $result.Data['greeting'] | Should -Be 'Hello World' + } + + It 'Export-Toml - round-trip preserves integer values' { + $inputDoc = [ordered]@{ count = [long]42 } + $path = Join-Path $TestDrive 'rt-int.toml' + Export-Toml -InputObject $inputDoc -Path $path + $result = Import-Toml -Path $path + $result.Data['count'] | Should -Be 42 + } + + It 'Export-Toml - round-trip preserves boolean values' { + $inputDoc = [ordered]@{ enabled = $true; disabled = $false } + $path = Join-Path $TestDrive 'rt-bool.toml' + Export-Toml -InputObject $inputDoc -Path $path + $result = Import-Toml -Path $path + $result.Data['enabled'] | Should -Be $true + $result.Data['disabled'] | Should -Be $false + } + + It 'Export-Toml - round-trip preserves float values' { + $inputDoc = [ordered]@{ pi = 3.14159 } + $path = Join-Path $TestDrive 'rt-float.toml' + Export-Toml -InputObject $inputDoc -Path $path + $result = Import-Toml -Path $path + ([double]$result.Data['pi'] - 3.14159) | Should -BeLessThan 0.00001 + } + + It 'Export-Toml - round-trip preserves nested tables' { + $inputDoc = [ordered]@{ + server = [ordered]@{ host = 'localhost'; port = [long]8080 } + } + $path = Join-Path $TestDrive 'rt-nested.toml' + Export-Toml -InputObject $inputDoc -Path $path + $result = Import-Toml -Path $path + $result.Data['server']['host'] | Should -Be 'localhost' + $result.Data['server']['port'] | Should -Be 8080 + } + + It 'Export-Toml - round-trip preserves integer arrays' { + $inputDoc = [ordered]@{ ports = @([long]80, [long]443) } + $path = Join-Path $TestDrive 'rt-array.toml' + Export-Toml -InputObject $inputDoc -Path $path + $result = Import-Toml -Path $path + $result.Data['ports'] | Should -HaveCount 2 + $result.Data['ports'][0] | Should -Be 80 + } + + It 'Export-Toml - full file round-trip' { + $srcPath = Join-Path $PSScriptRoot 'data\full-example.toml' + $original = Import-Toml -Path $srcPath + + $outPath = Join-Path $TestDrive 'rt-full.toml' + Export-Toml -InputObject $original -Path $outPath + + $rt = Import-Toml -Path $outPath + $rt.Data['title'] | Should -Be 'TOML Example' + $rt.Data['owner']['name'] | Should -Be 'Tom Preston-Werner' + $rt.Data['database']['enabled'] | Should -Be $true + $rt.Data['servers']['alpha']['ip'] | Should -Be '10.0.0.1' + } + } + + Context 'Export-Toml - UTF-8 encoding' { + It 'Export-Toml - writes UTF-8 without BOM' { + $path = Join-Path $TestDrive 'utf8.toml' + Export-Toml -InputObject ([ordered]@{ key = 'αβγ' }) -Path $path + $bytes = [System.IO.File]::ReadAllBytes($path) + # UTF-8 BOM is EF BB BF — should not be present + if ($bytes.Length -ge 3) { + ($bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) | Should -BeFalse + } + } + + It 'Export-Toml - round-trips Unicode string content correctly' { + $path = Join-Path $TestDrive 'unicode.toml' + Export-Toml -InputObject ([ordered]@{ msg = 'こんにちは' }) -Path $path + $result = Import-Toml -Path $path + $result.Data['msg'] | Should -Be 'こんにちは' + } + } + + Context 'Export-Toml - pipeline input' { + It 'Export-Toml - accepts TomlDocument from pipeline' { + $srcPath = Join-Path $PSScriptRoot 'data\booleans.toml' + $outPath = Join-Path $TestDrive 'from-pipeline.toml' + Import-Toml -Path $srcPath | Export-Toml -Path $outPath + Test-Path $outPath | Should -BeTrue + $rt = Import-Toml -Path $outPath + $rt.Data['enabled'] | Should -Be $true + } + } + + Context 'Export-Toml - WhatIf support' { + It 'Export-Toml - WhatIf does not create file' { + $path = Join-Path $TestDrive 'whatif.toml' + Export-Toml -InputObject ([ordered]@{ k = 'v' }) -Path $path -WhatIf + Test-Path -Path $path | Should -BeFalse + } + } + + Context 'Export-Toml - error handling' { + It 'Export-Toml - throws on null InputObject' { + $path = Join-Path $TestDrive 'null.toml' + { Export-Toml -InputObject $null -Path $path } | Should -Throw + } + } + } +} diff --git a/tests/Import-Toml.Tests.ps1 b/tests/Import-Toml.Tests.ps1 new file mode 100644 index 0000000..8668d83 --- /dev/null +++ b/tests/Import-Toml.Tests.ps1 @@ -0,0 +1,114 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester tests' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester tests' +)] +[CmdletBinding()] +param() + +Describe 'Toml' { + Describe 'Import-Toml' { + BeforeAll { + . (Join-Path $PSScriptRoot 'bootstrap.ps1') + $testDataDir = Join-Path $PSScriptRoot 'data' + } + + Context 'Import-Toml - return type' { + It 'Import-Toml - returns a TomlDocument' { + $path = Join-Path $testDataDir 'full-example.toml' + $result = Import-Toml -Path $path + $result.GetType().Name | Should -Be 'TomlDocument' + } + + It 'Import-Toml - sets FilePath to resolved absolute path' { + $path = Join-Path $testDataDir 'full-example.toml' + $result = Import-Toml -Path $path + $result.FilePath | Should -Not -BeNullOrEmpty + [System.IO.Path]::IsPathRooted($result.FilePath) | Should -BeTrue + } + + It 'Import-Toml - Data property is an OrderedDictionary' { + $path = Join-Path $testDataDir 'full-example.toml' + $result = Import-Toml -Path $path + $result.Data | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] + } + } + + Context 'Import-Toml - file content' { + It 'Import-Toml - reads all keys from full example file' { + $path = Join-Path $testDataDir 'full-example.toml' + $result = Import-Toml -Path $path + $result.Data['title'] | Should -Be 'TOML Example' + $result.Data['owner']['name'] | Should -Be 'Tom Preston-Werner' + $result.Data['database']['enabled'] | Should -Be $true + } + + It 'Import-Toml - reads integer file correctly' { + $path = Join-Path $testDataDir 'integers.toml' + $result = Import-Toml -Path $path + $result.Data['decimal'] | Should -Be 42 + $result.Data['negative'] | Should -Be -17 + $result.Data['hex'] | Should -Be 3735928559 # 0xDEADBEEF as unsigned long + } + + It 'Import-Toml - reads float file correctly' { + $path = Join-Path $testDataDir 'floats.toml' + $result = Import-Toml -Path $path + ([double]$result.Data['positive'] - 3.1415) | Should -BeLessThan 0.0001 + [double]::IsNaN($result.Data['special_nan']) | Should -BeTrue + } + + It 'Import-Toml - reads boolean file correctly' { + $path = Join-Path $testDataDir 'booleans.toml' + $result = Import-Toml -Path $path + $result.Data['enabled'] | Should -Be $true + $result.Data['disabled'] | Should -Be $false + } + + It 'Import-Toml - reads datetime file correctly' { + $path = Join-Path $testDataDir 'datetimes.toml' + $result = Import-Toml -Path $path + $result.Data['offset_dt_z'] | Should -BeOfType [System.DateTimeOffset] + $result.Data['local_date'] | Should -BeOfType [System.DateTime] + $result.Data['local_time'] | Should -BeOfType [System.TimeSpan] + } + + It 'Import-Toml - reads array file correctly' { + $path = Join-Path $testDataDir 'arrays.toml' + $result = Import-Toml -Path $path + $result.Data['integers'] | Should -HaveCount 3 + $result.Data['empty'] | Should -HaveCount 0 + } + + It 'Import-Toml - reads array-of-tables file correctly' { + $path = Join-Path $testDataDir 'array-of-tables.toml' + $result = Import-Toml -Path $path + $result.Data['products'] | Should -HaveCount 2 + $result.Data['products'][0]['name'] | Should -Be 'Hammer' + } + } + + Context 'Import-Toml - pipeline' { + It 'Import-Toml - accepts path from pipeline' { + $path = Join-Path $testDataDir 'booleans.toml' + $result = $path | Import-Toml + $result.GetType().Name | Should -Be 'TomlDocument' + } + } + + Context 'Import-Toml - error handling' { + It 'Import-Toml - throws when file does not exist' { + { Import-Toml -Path 'nonexistent-file-that-does-not-exist.toml' } | Should -Throw + } + + It 'Import-Toml - throws when file contains invalid TOML' { + $badToml = Join-Path $TestDrive 'bad.toml' + Set-Content -Path $badToml -Value 'invalid = = "bad"' + { Import-Toml -Path $badToml } | Should -Throw + } + } + } +} diff --git a/tests/Toml.Tests.ps1 b/tests/Toml.Tests.ps1 index dfdbc33..c77ae33 100644 --- a/tests/Toml.Tests.ps1 +++ b/tests/Toml.Tests.ps1 @@ -10,7 +10,15 @@ param() Describe 'Module' { - It 'Function: ConvertFrom-Toml - Throws NotImplementedException' { - { ConvertFrom-Toml -InputObject '[database]' } | Should -Throw + BeforeAll { + . (Join-Path $PSScriptRoot 'bootstrap.ps1') + } + + It 'Module has expected commands' { + $commands = Get-Command -Module Toml | Select-Object -ExpandProperty Name | Sort-Object + $commands | Should -Contain 'ConvertFrom-Toml' + $commands | Should -Contain 'ConvertTo-Toml' + $commands | Should -Contain 'Import-Toml' + $commands | Should -Contain 'Export-Toml' } } diff --git a/tests/bootstrap.ps1 b/tests/bootstrap.ps1 new file mode 100644 index 0000000..41f1bd8 --- /dev/null +++ b/tests/bootstrap.ps1 @@ -0,0 +1,17 @@ +<# + .SYNOPSIS + Bootstrap helper for local Pester runs. + + .DESCRIPTION + Imports the built Toml module from ./output/Toml/ before tests run. + Call from a BeforeAll block in every test file. +#> +[CmdletBinding()] +param() + +$outputManifest = Join-Path $PSScriptRoot '..' 'output' 'Toml' 'Toml.psd1' +if (-not (Test-Path $outputManifest)) { + throw "Module manifest not found at '$outputManifest'. Run build.ps1 first." +} + +Import-Module $outputManifest -Force -ErrorAction Stop diff --git a/tests/data/array-of-tables.toml b/tests/data/array-of-tables.toml new file mode 100644 index 0000000..5117a59 --- /dev/null +++ b/tests/data/array-of-tables.toml @@ -0,0 +1,21 @@ +[[products]] +name = "Hammer" +sku = 738594937 + +[[products]] +name = "Nail" +sku = 284758393 +color = "gray" + +[[fruits]] +name = "apple" + +[fruits.physical] +color = "red" +shape = "round" + +[[fruits.varieties]] +name = "red delicious" + +[[fruits.varieties]] +name = "granny smith" diff --git a/tests/data/arrays.toml b/tests/data/arrays.toml new file mode 100644 index 0000000..1ce7fd0 --- /dev/null +++ b/tests/data/arrays.toml @@ -0,0 +1,11 @@ +# Array types +integers = [1, 2, 3] +strings = ["a", "b", "c"] +mixed_types = [1, "two", 3.0, true] +nested = [[1, 2], [3, 4, 5]] +empty = [] +multiline = [ + 1, + 2, + 3, +] diff --git a/tests/data/booleans.toml b/tests/data/booleans.toml new file mode 100644 index 0000000..db0df2c --- /dev/null +++ b/tests/data/booleans.toml @@ -0,0 +1,3 @@ +# Boolean values +enabled = true +disabled = false diff --git a/tests/data/datetimes.toml b/tests/data/datetimes.toml new file mode 100644 index 0000000..5a53ae5 --- /dev/null +++ b/tests/data/datetimes.toml @@ -0,0 +1,6 @@ +# Date and time types +offset_dt_z = 1979-05-27T07:32:00Z +offset_dt_num = 1979-05-27T07:32:00+05:30 +local_dt = 1979-05-27T07:32:00 +local_date = 1979-05-27 +local_time = 07:32:00 diff --git a/tests/data/floats.toml b/tests/data/floats.toml new file mode 100644 index 0000000..3da6357 --- /dev/null +++ b/tests/data/floats.toml @@ -0,0 +1,9 @@ +# Float values +positive = 3.1415 +negative = -0.01 +exponent = 5e+22 +large = 1e6 +combo = 6.626e-34 +special_inf = inf +special_neg_inf = -inf +special_nan = nan diff --git a/tests/data/full-example.toml b/tests/data/full-example.toml new file mode 100644 index 0000000..00a6050 --- /dev/null +++ b/tests/data/full-example.toml @@ -0,0 +1,22 @@ +# Full TOML 1.0.0 example - valid document +title = "TOML Example" + +[owner] +name = "Tom Preston-Werner" +dob = 1979-05-27T07:32:00-08:00 + +[database] +enabled = true +ports = [ 8001, 8001, 8002 ] +data = [ ["delta", "phi"], [3.14] ] +temp_targets = { cpu = 79.5, case = 72.0 } + +[servers] + +[servers.alpha] +ip = "10.0.0.1" +role = "frontend" + +[servers.beta] +ip = "10.0.0.2" +role = "backend" diff --git a/tests/data/integers.toml b/tests/data/integers.toml new file mode 100644 index 0000000..9d9e862 --- /dev/null +++ b/tests/data/integers.toml @@ -0,0 +1,9 @@ +# All integer representations +decimal = 42 +negative = -17 +positive = +99 +zero = 0 +large = 9_007_199_254_740_992 +hex = 0xDEADBEEF +octal = 0o755 +binary = 0b11010110 diff --git a/tests/data/strings.toml b/tests/data/strings.toml new file mode 100644 index 0000000..ae05e4c --- /dev/null +++ b/tests/data/strings.toml @@ -0,0 +1,12 @@ +# Strings - all four TOML string types +basic = "I'm a string. \"You can quote me\". Name\tTab\nNewLine." +literal = 'C:\Users\nodejs\templates' +multiline_basic = """ +Roses are red +Violets are blue""" +multiline_literal = ''' +First paragraph. + +Second paragraph.''' +escaped_unicode = "\u03B1\u03B2\u03B3" +empty = "" diff --git a/tests/data/tables.toml b/tests/data/tables.toml new file mode 100644 index 0000000..c857d63 --- /dev/null +++ b/tests/data/tables.toml @@ -0,0 +1,9 @@ +# Table types +[simple] +key = "value" + +[dotted.key] +works = true + +[inline_parent] +inline = { one = 1, two = 2 } From dc1d3ba58990ac8818be671624167c5e2e02e1be Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 23 Jul 2026 18:50:28 +0200 Subject: [PATCH 02/13] =?UTF-8?q?feat!:=20Pure=20PowerShell=20TOML=201.0.0?= =?UTF-8?q?=20module=20=E2=80=94=20full=20parser,=20serializer,=20advanced?= =?UTF-8?q?=20fixtures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace Tomlyn (.NET) dependency with pure PowerShell parser/serializer - Add 19 private helper functions covering all TOML token types - Fix: sub-tables inside array-of-tables entries no longer falsely fail as redefinitions - Fix: space-separator datetimes (e.g. '1987-07-05 17:45:00Z') now parsed correctly - Add 5 advanced TOML fixture files covering deep AoT nesting, all numeric bases, all datetime forms, Unicode escapes, multi-line strings, and heterogeneous arrays - Remove src/assemblies/Tomlyn.dll and all Tomlyn* helpers - Set PowerShellVersion = 7.6 in manifest - 116 tests pass, PSScriptAnalyzer clean Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 4 +- build.ps1 | 23 +-- src/assemblies/Tomlyn.dll | Bin 325120 -> 0 bytes .../private/Toml/Add-TomlTableText.ps1 | 74 ++++++++ .../Toml/ConvertFrom-TomlBasicStringValue.ps1 | 71 ++++++++ .../private/Toml/ConvertFrom-TomlDateTime.ps1 | 77 ++++---- .../ConvertFrom-TomlLiteralStringValue.ps1 | 39 ++++ .../Toml/ConvertFrom-TomlParsedValue.ps1 | 103 +++++++++++ .../Toml/ConvertFrom-TomlScalarToken.ps1 | 57 ++++++ .../private/Toml/ConvertFrom-TomlTable.ps1 | 169 ++++++++++++++++++ .../private/Toml/ConvertFrom-TomlValue.ps1 | 27 +++ .../private/Toml/ConvertFrom-TomlynTable.ps1 | 43 ----- .../private/Toml/ConvertFrom-TomlynValue.ps1 | 74 -------- .../Toml/ConvertTo-TomlArrayObject.ps1 | 19 ++ .../Toml/ConvertTo-TomlTableObject.ps1 | 51 ++++++ .../private/Toml/ConvertTo-TomlValue.ps1 | 83 +++++++++ .../private/Toml/ConvertTo-TomlynArray.ps1 | 41 ----- .../private/Toml/ConvertTo-TomlynTable.ps1 | 55 ------ .../private/Toml/ConvertTo-TomlynValue.ps1 | 128 ------------- src/functions/private/Toml/Format-TomlKey.ps1 | 19 ++ .../private/Toml/Get-TomlBareToken.ps1 | 32 ++++ .../Toml/Get-TomlContentWithoutComment.ps1 | 64 +++++++ .../private/Toml/Get-TomlNestedTable.ps1 | 48 +++++ .../private/Toml/Join-TomlKeyPath.ps1 | 19 ++ .../private/Toml/Skip-TomlWhitespace.ps1 | 18 ++ .../private/Toml/Split-TomlDottedKey.ps1 | 86 +++++++++ .../Toml/Test-TomlEndsWithDoubleNewLine.ps1 | 20 +++ .../private/Toml/Test-TomlTableArray.ps1 | 38 ++++ .../public/Toml/ConvertFrom-Toml.ps1 | 82 +-------- src/functions/public/Toml/ConvertTo-Toml.ps1 | 79 +------- src/init/initializer.ps1 | 8 +- src/manifest.psd1 | 3 +- tests/data/advanced/app-config.toml | 117 ++++++++++++ tests/data/advanced/cargo-manifest.toml | 95 ++++++++++ tests/data/advanced/ci-pipeline.toml | 78 ++++++++ tests/data/advanced/database-server.toml | 164 +++++++++++++++++ .../data/advanced/numbers-and-datetimes.toml | 103 +++++++++++ 37 files changed, 1646 insertions(+), 565 deletions(-) delete mode 100644 src/assemblies/Tomlyn.dll create mode 100644 src/functions/private/Toml/Add-TomlTableText.ps1 create mode 100644 src/functions/private/Toml/ConvertFrom-TomlBasicStringValue.ps1 create mode 100644 src/functions/private/Toml/ConvertFrom-TomlLiteralStringValue.ps1 create mode 100644 src/functions/private/Toml/ConvertFrom-TomlParsedValue.ps1 create mode 100644 src/functions/private/Toml/ConvertFrom-TomlScalarToken.ps1 create mode 100644 src/functions/private/Toml/ConvertFrom-TomlTable.ps1 create mode 100644 src/functions/private/Toml/ConvertFrom-TomlValue.ps1 delete mode 100644 src/functions/private/Toml/ConvertFrom-TomlynTable.ps1 delete mode 100644 src/functions/private/Toml/ConvertFrom-TomlynValue.ps1 create mode 100644 src/functions/private/Toml/ConvertTo-TomlArrayObject.ps1 create mode 100644 src/functions/private/Toml/ConvertTo-TomlTableObject.ps1 create mode 100644 src/functions/private/Toml/ConvertTo-TomlValue.ps1 delete mode 100644 src/functions/private/Toml/ConvertTo-TomlynArray.ps1 delete mode 100644 src/functions/private/Toml/ConvertTo-TomlynTable.ps1 delete mode 100644 src/functions/private/Toml/ConvertTo-TomlynValue.ps1 create mode 100644 src/functions/private/Toml/Format-TomlKey.ps1 create mode 100644 src/functions/private/Toml/Get-TomlBareToken.ps1 create mode 100644 src/functions/private/Toml/Get-TomlContentWithoutComment.ps1 create mode 100644 src/functions/private/Toml/Get-TomlNestedTable.ps1 create mode 100644 src/functions/private/Toml/Join-TomlKeyPath.ps1 create mode 100644 src/functions/private/Toml/Skip-TomlWhitespace.ps1 create mode 100644 src/functions/private/Toml/Split-TomlDottedKey.ps1 create mode 100644 src/functions/private/Toml/Test-TomlEndsWithDoubleNewLine.ps1 create mode 100644 src/functions/private/Toml/Test-TomlTableArray.ps1 create mode 100644 tests/data/advanced/app-config.toml create mode 100644 tests/data/advanced/cargo-manifest.toml create mode 100644 tests/data/advanced/ci-pipeline.toml create mode 100644 tests/data/advanced/database-server.toml create mode 100644 tests/data/advanced/numbers-and-datetimes.toml diff --git a/README.md b/README.md index 7104745..6e4b639 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ PowerShell module for reading and writing [TOML](https://toml.io) data, with ful ## Prerequisites -- PowerShell 7.2 or later +- PowerShell 7.6 LTS - The [PSModule framework](https://github.com/PSModule/Process-PSModule) for building, testing and publishing the module. ## Installation @@ -105,7 +105,7 @@ Export-Toml -InputObject $doc -Path './config.toml' ## Implementation notes -- Backed by [Tomlyn v2.0.0](https://github.com/xoofx/Tomlyn), a conformant .NET TOML 1.0.0 library. +- Implemented with in-repository PowerShell parser/serializer logic (no external TOML runtime dependency). - Duplicate keys and table redefinition are rejected per the TOML 1.0.0 spec. - Files are written as UTF-8 without BOM. diff --git a/build.ps1 b/build.ps1 index b9572f7..b702126 100644 --- a/build.ps1 +++ b/build.ps1 @@ -26,12 +26,6 @@ if (Test-Path $outputPath) { } $null = New-Item -ItemType Directory -Path $outputPath -Force -# ── copy assemblies ────────────────────────────────────────────────────────── -$assembliesSrc = Join-Path $srcPath 'assemblies' -$assembliesDst = Join-Path $outputPath 'assemblies' -$null = New-Item -ItemType Directory -Path $assembliesDst -Force -Copy-Item -Path (Join-Path $assembliesSrc '*.dll') -Destination $assembliesDst -ErrorAction SilentlyContinue - # ── build psm1 ─────────────────────────────────────────────────────────────── $psm1Path = Join-Path $outputPath "$moduleName.psm1" $sb = [System.Text.StringBuilder]::new() @@ -42,19 +36,10 @@ if (Test-Path $headerPath) { $null = $sb.AppendLine((Get-Content $headerPath -Raw)) } -# init — emit a corrected version that uses the assembled module's own PSScriptRoot -# The source init uses '../assemblies/Tomlyn.dll' relative to src/init/. -# In the built module, assemblies live next to the psm1, so we rewrite the path. -$initContent = @' -$assemblyPath = Join-Path -Path $PSScriptRoot -ChildPath 'assemblies\Tomlyn.dll' -$resolvedPath = [System.IO.Path]::GetFullPath($assemblyPath) - -if (-not [System.AppDomain]::CurrentDomain.GetAssemblies().Where({ $_.GetName().Name -eq 'Tomlyn' })) { - [System.Reflection.Assembly]::LoadFrom($resolvedPath) | Out-Null - Write-Verbose "Loaded Tomlyn assembly from: $resolvedPath" +# init +foreach ($f in (Get-ChildItem (Join-Path $srcPath 'init') -Filter '*.ps1' -ErrorAction SilentlyContinue)) { + $null = $sb.AppendLine((Get-Content $f.FullName -Raw)) } -'@ -$null = $sb.AppendLine($initContent) # classes private then public foreach ($visibility in @('private', 'public')) { @@ -94,7 +79,7 @@ $manifest = @{ ModuleVersion = $ModuleVersion RootModule = "$moduleName.psm1" FunctionsToExport = $publicFunctions.ToArray() - PowerShellVersion = '7.2' + PowerShellVersion = '7.6' Description = 'PowerShell module for reading and writing TOML data.' Author = 'PSModule' CompanyName = 'PSModule' diff --git a/src/assemblies/Tomlyn.dll b/src/assemblies/Tomlyn.dll deleted file mode 100644 index b76eee42ab70536a09479b65690c46a01cb4885b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 325120 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 diff --git a/src/functions/private/Toml/Add-TomlTableText.ps1 b/src/functions/private/Toml/Add-TomlTableText.ps1 new file mode 100644 index 0000000..412fa57 --- /dev/null +++ b/src/functions/private/Toml/Add-TomlTableText.ps1 @@ -0,0 +1,74 @@ +function Add-TomlTableText { + <# + .SYNOPSIS + Appends TOML text for a table to a string builder. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [System.Text.StringBuilder] $StringBuilder, + + [Parameter(Mandatory)] + [System.Collections.Specialized.OrderedDictionary] $Table, + + [Parameter(Mandatory)] + [AllowEmptyString()] + [string] $Path, + + [Parameter(Mandatory)] + [bool] $EmitHeader + ) + + if ($EmitHeader -and -not [string]::IsNullOrEmpty($Path)) { + if ($StringBuilder.Length -gt 0 -and -not (Test-TomlEndsWithDoubleNewLine -StringBuilder $StringBuilder)) { + $null = $StringBuilder.AppendLine() + } + $null = $StringBuilder.AppendLine("[$Path]") + } + + foreach ($key in $Table.Keys) { + $value = $Table[$key] + if ($value -is [System.Collections.Specialized.OrderedDictionary]) { + continue + } + if (Test-TomlTableArray -Value $value) { + continue + } + $null = $StringBuilder.AppendLine("$(Format-TomlKey -Key $key) = $(ConvertTo-TomlValue -Value $value)") + } + + foreach ($key in $Table.Keys) { + $value = $Table[$key] + if ($value -isnot [System.Collections.Specialized.OrderedDictionary]) { + continue + } + + $childPath = if ([string]::IsNullOrEmpty($Path)) { + Format-TomlKey -Key $key + } else { + "$Path.$(Format-TomlKey -Key $key)" + } + Add-TomlTableText -StringBuilder $StringBuilder -Table $value -Path $childPath -EmitHeader:$true + } + + foreach ($key in $Table.Keys) { + $value = $Table[$key] + if (-not (Test-TomlTableArray -Value $value)) { + continue + } + + $arrayPath = if ([string]::IsNullOrEmpty($Path)) { + Format-TomlKey -Key $key + } else { + "$Path.$(Format-TomlKey -Key $key)" + } + + foreach ($item in $value) { + if ($StringBuilder.Length -gt 0 -and -not (Test-TomlEndsWithDoubleNewLine -StringBuilder $StringBuilder)) { + $null = $StringBuilder.AppendLine() + } + $null = $StringBuilder.AppendLine("[[$arrayPath]]") + Add-TomlTableText -StringBuilder $StringBuilder -Table $item -Path $arrayPath -EmitHeader:$false + } + } +} diff --git a/src/functions/private/Toml/ConvertFrom-TomlBasicStringValue.ps1 b/src/functions/private/Toml/ConvertFrom-TomlBasicStringValue.ps1 new file mode 100644 index 0000000..669e99d --- /dev/null +++ b/src/functions/private/Toml/ConvertFrom-TomlBasicStringValue.ps1 @@ -0,0 +1,71 @@ +function ConvertFrom-TomlBasicStringValue { + <# + .SYNOPSIS + Parses a TOML basic string at the current source index. + #> + [OutputType([string])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Source, + + [Parameter(Mandatory)] + [ref] $Index + ) + + if ($Source.Substring($Index.Value).StartsWith('"""', [System.StringComparison]::Ordinal)) { + $Index.Value += 3 + $start = $Index.Value + $end = $Source.IndexOf('"""', $start, [System.StringComparison]::Ordinal) + if ($end -lt 0) { + throw [System.InvalidOperationException]::new('Unterminated multi-line basic string.') + } + $Index.Value = $end + 3 + return $Source.Substring($start, $end - $start) + } + + $Index.Value++ + $sb = [System.Text.StringBuilder]::new() + while ($Index.Value -lt $Source.Length) { + $ch = $Source[$Index.Value] + if ($ch -eq '"') { + $Index.Value++ + return $sb.ToString() + } + + if ($ch -ne '\') { + $null = $sb.Append($ch) + $Index.Value++ + continue + } + + $Index.Value++ + if ($Index.Value -ge $Source.Length) { + throw [System.InvalidOperationException]::new('Invalid trailing backslash in TOML string.') + } + $esc = $Source[$Index.Value] + switch ($esc) { + '"' { $null = $sb.Append('"') } + '\' { $null = $sb.Append('\') } + 'b' { $null = $sb.Append("`b") } + 't' { $null = $sb.Append("`t") } + 'n' { $null = $sb.Append("`n") } + 'f' { $null = $sb.Append("`f") } + 'r' { $null = $sb.Append("`r") } + 'u' { + if (($Index.Value + 4) -ge $Source.Length) { + throw [System.InvalidOperationException]::new('Invalid \u escape in TOML string.') + } + $hex = $Source.Substring($Index.Value + 1, 4) + $null = $sb.Append([char][Convert]::ToInt32($hex, 16)) + $Index.Value += 4 + } + default { + throw [System.InvalidOperationException]::new("Unsupported TOML escape sequence '\$esc'.") + } + } + $Index.Value++ + } + + throw [System.InvalidOperationException]::new('Unterminated TOML basic string.') +} diff --git a/src/functions/private/Toml/ConvertFrom-TomlDateTime.ps1 b/src/functions/private/Toml/ConvertFrom-TomlDateTime.ps1 index eb9be73..2c4798d 100644 --- a/src/functions/private/Toml/ConvertFrom-TomlDateTime.ps1 +++ b/src/functions/private/Toml/ConvertFrom-TomlDateTime.ps1 @@ -1,59 +1,44 @@ -function ConvertFrom-TomlDateTime { +function ConvertFrom-TomlDateTime { <# .SYNOPSIS - Converts a Tomlyn TomlDateTime to the appropriate PowerShell date/time type. + Converts a TOML date/time token to a PowerShell date/time value. .DESCRIPTION - Maps each TOML date/time variant to the most natural PowerShell type: - - OffsetDateTimeByZ / OffsetDateTimeByNumber -> [System.DateTimeOffset] - - LocalDateTime -> [System.DateTime] (Unspecified Kind) - - LocalDate -> [System.DateTime] (date only, 00:00:00) - - LocalTime -> [System.TimeSpan] (time of day) - - .EXAMPLE - ConvertFrom-TomlDateTime -TomlDt $tomlDateTimeValue - - Returns a [DateTimeOffset], [DateTime], or [TimeSpan] depending on kind. - - .INPUTS - [Tomlyn.TomlDateTime] - - .OUTPUTS - [object] — [System.DateTimeOffset], [System.DateTime], or [System.TimeSpan] - - .NOTES - Internal helper. Not exported. No pipeline input. + Parses TOML local date, local time, local date-time, and offset date-time + tokens and returns the corresponding PowerShell type. #> [OutputType([System.DateTimeOffset], [System.DateTime], [System.TimeSpan])] [CmdletBinding()] param( - # The Tomlyn TomlDateTime to convert. [Parameter(Mandatory)] - [Tomlyn.TomlDateTime] $TomlDt + [ValidateNotNullOrEmpty()] + [string] $Token ) - switch ($TomlDt.Kind) { - ([Tomlyn.TomlDateTimeKind]::OffsetDateTimeByZ) { - return $TomlDt.DateTime - } - ([Tomlyn.TomlDateTimeKind]::OffsetDateTimeByNumber) { - return $TomlDt.DateTime - } - ([Tomlyn.TomlDateTimeKind]::LocalDateTime) { - # Strip offset — keep year/month/day/hour/minute/second as-is - $utc = $TomlDt.DateTime.DateTime - return [System.DateTime]::SpecifyKind($utc, [System.DateTimeKind]::Unspecified) - } - ([Tomlyn.TomlDateTimeKind]::LocalDate) { - $utc = $TomlDt.DateTime.Date - return [System.DateTime]::SpecifyKind($utc, [System.DateTimeKind]::Unspecified) - } - ([Tomlyn.TomlDateTimeKind]::LocalTime) { - return $TomlDt.DateTime.TimeOfDay - } - default { - # Fallback: return DateTimeOffset - return $TomlDt.DateTime - } + $value = $Token.Trim() + $culture = [System.Globalization.CultureInfo]::InvariantCulture + $dateStyles = [System.Globalization.DateTimeStyles]::None + + if ($value -match '^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+\-]\d{2}:\d{2})$') { + $normalized = $value -replace '^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})', '$1T$2' + $dto = [System.DateTimeOffset]::Parse($normalized, $culture, $dateStyles) + return $dto } + + if ($value -match '^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?$') { + $normalized = $value -replace '^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})', '$1T$2' + $dt = [System.DateTime]::Parse($normalized, $culture, $dateStyles) + return [System.DateTime]::SpecifyKind($dt, [System.DateTimeKind]::Unspecified) + } + + if ($value -match '^\d{4}-\d{2}-\d{2}$') { + $dt = [System.DateTime]::ParseExact($value, 'yyyy-MM-dd', $culture, $dateStyles) + return [System.DateTime]::SpecifyKind($dt, [System.DateTimeKind]::Unspecified) + } + + if ($value -match '^\d{2}:\d{2}:\d{2}(?:\.\d+)?$') { + return [System.TimeSpan]::Parse($value, $culture) + } + + throw [System.InvalidOperationException]::new("Invalid TOML date/time token: '$Token'.") } diff --git a/src/functions/private/Toml/ConvertFrom-TomlLiteralStringValue.ps1 b/src/functions/private/Toml/ConvertFrom-TomlLiteralStringValue.ps1 new file mode 100644 index 0000000..12a49fb --- /dev/null +++ b/src/functions/private/Toml/ConvertFrom-TomlLiteralStringValue.ps1 @@ -0,0 +1,39 @@ +function ConvertFrom-TomlLiteralStringValue { + <# + .SYNOPSIS + Parses a TOML literal string at the current source index. + #> + [OutputType([string])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Source, + + [Parameter(Mandatory)] + [ref] $Index + ) + + if ($Source.Substring($Index.Value).StartsWith("'''", [System.StringComparison]::Ordinal)) { + $Index.Value += 3 + $start = $Index.Value + $end = $Source.IndexOf("'''", $start, [System.StringComparison]::Ordinal) + if ($end -lt 0) { + throw [System.InvalidOperationException]::new('Unterminated multi-line literal string.') + } + $Index.Value = $end + 3 + return $Source.Substring($start, $end - $start) + } + + $Index.Value++ + $start = $Index.Value + while ($Index.Value -lt $Source.Length) { + if ($Source[$Index.Value] -eq '''') { + $literal = $Source.Substring($start, $Index.Value - $start) + $Index.Value++ + return $literal + } + $Index.Value++ + } + + throw [System.InvalidOperationException]::new('Unterminated TOML literal string.') +} diff --git a/src/functions/private/Toml/ConvertFrom-TomlParsedValue.ps1 b/src/functions/private/Toml/ConvertFrom-TomlParsedValue.ps1 new file mode 100644 index 0000000..952afa8 --- /dev/null +++ b/src/functions/private/Toml/ConvertFrom-TomlParsedValue.ps1 @@ -0,0 +1,103 @@ +function ConvertFrom-TomlParsedValue { + <# + .SYNOPSIS + Parses a TOML value at the current source index. + #> + [OutputType([object])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Source, + + [Parameter(Mandatory)] + [ref] $Index + ) + + Skip-TomlWhitespace -Source $Source -Index $Index + if ($Index.Value -ge $Source.Length) { + throw [System.InvalidOperationException]::new('Unexpected end of TOML value.') + } + + $ch = $Source[$Index.Value] + if ($ch -eq '"') { + return ConvertFrom-TomlBasicStringValue -Source $Source -Index $Index + } + if ($ch -eq '''') { + return ConvertFrom-TomlLiteralStringValue -Source $Source -Index $Index + } + + if ($ch -eq '[') { + $Index.Value++ + $items = [System.Collections.ArrayList]::new() + while ($true) { + Skip-TomlWhitespace -Source $Source -Index $Index + if ($Index.Value -ge $Source.Length) { + throw [System.InvalidOperationException]::new('Unterminated TOML array.') + } + if ($Source[$Index.Value] -eq ']') { + $Index.Value++ + break + } + + $null = $items.Add((ConvertFrom-TomlParsedValue -Source $Source -Index $Index)) + Skip-TomlWhitespace -Source $Source -Index $Index + if ($Index.Value -lt $Source.Length -and $Source[$Index.Value] -eq ',') { + $Index.Value++ + continue + } + if ($Index.Value -lt $Source.Length -and $Source[$Index.Value] -eq ']') { + continue + } + if ($Index.Value -ge $Source.Length) { + throw [System.InvalidOperationException]::new('Unterminated TOML array.') + } + throw [System.InvalidOperationException]::new('Invalid TOML array separator.') + } + return $items.ToArray() + } + + if ($ch -eq '{') { + $Index.Value++ + $dict = [System.Collections.Specialized.OrderedDictionary]::new([System.StringComparer]::Ordinal) + while ($true) { + Skip-TomlWhitespace -Source $Source -Index $Index + if ($Index.Value -ge $Source.Length) { + throw [System.InvalidOperationException]::new('Unterminated TOML inline table.') + } + + if ($Source[$Index.Value] -eq '}') { + $Index.Value++ + break + } + + $key = if ($Source[$Index.Value] -eq '"') { + ConvertFrom-TomlBasicStringValue -Source $Source -Index $Index + } elseif ($Source[$Index.Value] -eq '''') { + ConvertFrom-TomlLiteralStringValue -Source $Source -Index $Index + } else { + Get-TomlBareToken -Source $Source -Index $Index -StopAtEquals $true + } + + Skip-TomlWhitespace -Source $Source -Index $Index + if ($Index.Value -ge $Source.Length -or $Source[$Index.Value] -ne '=') { + throw [System.InvalidOperationException]::new("Invalid TOML inline table entry for key '$key'.") + } + $Index.Value++ + + $dict[$key] = ConvertFrom-TomlParsedValue -Source $Source -Index $Index + Skip-TomlWhitespace -Source $Source -Index $Index + + if ($Index.Value -lt $Source.Length -and $Source[$Index.Value] -eq ',') { + $Index.Value++ + continue + } + if ($Index.Value -lt $Source.Length -and $Source[$Index.Value] -eq '}') { + continue + } + } + return $dict + } + + $token = Get-TomlBareToken -Source $Source -Index $Index + return ConvertFrom-TomlScalarToken -Token $token +} diff --git a/src/functions/private/Toml/ConvertFrom-TomlScalarToken.ps1 b/src/functions/private/Toml/ConvertFrom-TomlScalarToken.ps1 new file mode 100644 index 0000000..fc7339a --- /dev/null +++ b/src/functions/private/Toml/ConvertFrom-TomlScalarToken.ps1 @@ -0,0 +1,57 @@ +function ConvertFrom-TomlScalarToken { + <# + .SYNOPSIS + Converts a scalar TOML token to a PowerShell value. + #> + [OutputType([object])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Token + ) + + if ($Token -ceq 'true') { return $true } + if ($Token -ceq 'false') { return $false } + if ($Token -ceq 'inf' -or $Token -ceq '+inf') { return [double]::PositiveInfinity } + if ($Token -ceq '-inf') { return [double]::NegativeInfinity } + if ($Token -ceq 'nan' -or $Token -ceq '+nan' -or $Token -ceq '-nan') { return [double]::NaN } + + if ($Token -match '^\d{4}-\d{2}-\d{2}(?:[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+\-]\d{2}:\d{2})?)?$' -or + $Token -match '^\d{2}:\d{2}:\d{2}(?:\.\d+)?$') { + return ConvertFrom-TomlDateTime -Token $Token + } + + if ($Token -match '^[+\-]?0x[0-9A-Fa-f_]+$') { + $isNegative = $Token.StartsWith('-') + $hex = $Token.TrimStart('+', '-').Substring(2).Replace('_', '') + $number = [Convert]::ToUInt64($hex, 16) + if ($isNegative) { return - ([long]$number) } + return [long]$number + } + + if ($Token -match '^[+\-]?0o[0-7_]+$') { + $isNegative = $Token.StartsWith('-') + $oct = $Token.TrimStart('+', '-').Substring(2).Replace('_', '') + $number = [Convert]::ToInt64($oct, 8) + if ($isNegative) { return - $number } + return [long]$number + } + + if ($Token -match '^[+\-]?0b[01_]+$') { + $isNegative = $Token.StartsWith('-') + $bin = $Token.TrimStart('+', '-').Substring(2).Replace('_', '') + $number = [Convert]::ToInt64($bin, 2) + if ($isNegative) { return - $number } + return [long]$number + } + + if ($Token -match '^[+\-]?\d[\d_]*$') { + return [long]::Parse($Token.Replace('_', ''), [System.Globalization.CultureInfo]::InvariantCulture) + } + + if ($Token -match '^[+\-]?(?:\d[\d_]*\.\d[\d_]*|\d[\d_]*[eE][+\-]?\d[\d_]*|\d[\d_]*\.\d[\d_]*[eE][+\-]?\d[\d_]*)$') { + return [double]::Parse($Token.Replace('_', ''), [System.Globalization.CultureInfo]::InvariantCulture) + } + + throw [System.InvalidOperationException]::new("Unsupported or invalid TOML scalar value: '$Token'.") +} diff --git a/src/functions/private/Toml/ConvertFrom-TomlTable.ps1 b/src/functions/private/Toml/ConvertFrom-TomlTable.ps1 new file mode 100644 index 0000000..23bea8a --- /dev/null +++ b/src/functions/private/Toml/ConvertFrom-TomlTable.ps1 @@ -0,0 +1,169 @@ +function ConvertFrom-TomlTable { + <# + .SYNOPSIS + Parses a TOML document string into an ordered dictionary. + #> + [OutputType([System.Collections.Specialized.OrderedDictionary])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $InputObject + ) + + $root = [System.Collections.Specialized.OrderedDictionary]::new([System.StringComparer]::Ordinal) + $currentTable = $root + $definedHeaders = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + $arrayLastTableByPath = @{} + + $text = $InputObject -replace "`r`n", "`n" -replace "`r", "`n" + $lines = $text -split "`n" + + for ($i = 0; $i -lt $lines.Count; $i++) { + $rawLine = $lines[$i] + $line = (Get-TomlContentWithoutComment -Line $rawLine).Trim() + if ([string]::IsNullOrWhiteSpace($line)) { + continue + } + + if ($line.StartsWith('[[') -and $line.EndsWith(']]')) { + $header = $line.Substring(2, $line.Length - 4).Trim() + $pathSegments = Split-TomlDottedKey -KeyPath $header + if ($pathSegments.Count -eq 0) { + throw [System.InvalidOperationException]::new('Invalid array-of-tables header.') + } + + # Clear headers from sub-tables of any previous entry in this same array, + # so that each [[arr]] entry may independently define [arr.sub] headers. + $aoKeyPath = Join-TomlKeyPath -Segments $pathSegments + $toRemove = $definedHeaders | Where-Object { $_ -eq $aoKeyPath -or $_.StartsWith("$aoKeyPath.") } + foreach ($h in @($toRemove)) { + $null = $definedHeaders.Remove($h) + } + + $parentSegments = if ($pathSegments.Count -gt 1) { $pathSegments[0..($pathSegments.Count - 2)] } else { @() } + $name = $pathSegments[-1] + $parent = Get-TomlNestedTable -StartTable $root -Segments $parentSegments -ArrayLastTableByPath $arrayLastTableByPath + + if (-not $parent.Contains($name)) { + $parent[$name] = [System.Collections.ArrayList]::new() + } elseif ($parent[$name] -isnot [System.Collections.ArrayList]) { + throw [System.InvalidOperationException]::new("Cannot redefine '$header' as array-of-tables.") + } + + $entry = [System.Collections.Specialized.OrderedDictionary]::new([System.StringComparer]::Ordinal) + $null = $parent[$name].Add($entry) + $currentTable = $entry + $arrayLastTableByPath[(Join-TomlKeyPath -Segments $pathSegments)] = $entry + continue + } + + if ($line.StartsWith('[') -and $line.EndsWith(']')) { + $header = $line.Substring(1, $line.Length - 2).Trim() + $pathSegments = Split-TomlDottedKey -KeyPath $header + $headerPath = Join-TomlKeyPath -Segments $pathSegments + if ($definedHeaders.Contains($headerPath)) { + throw [System.InvalidOperationException]::new("The key '$headerPath' is already defined and cannot be redefined.") + } + $null = $definedHeaders.Add($headerPath) + + $parentSegments = if ($pathSegments.Count -gt 1) { $pathSegments[0..($pathSegments.Count - 2)] } else { @() } + $name = $pathSegments[-1] + $parent = Get-TomlNestedTable -StartTable $root -Segments $parentSegments -ArrayLastTableByPath $arrayLastTableByPath + + if ($parent.Contains($name)) { + if ($parent[$name] -is [System.Collections.Specialized.OrderedDictionary]) { + throw [System.InvalidOperationException]::new("The key '$headerPath' is already defined and cannot be redefined.") + } + throw [System.InvalidOperationException]::new("Cannot define table '$headerPath' because the key already exists as a non-table.") + } + + $parent[$name] = [System.Collections.Specialized.OrderedDictionary]::new([System.StringComparer]::Ordinal) + $currentTable = $parent[$name] + continue + } + + $eqIndex = $line.IndexOf('=') + if ($eqIndex -lt 1) { + throw [System.InvalidOperationException]::new("Invalid TOML assignment on line $($i + 1): '$rawLine'.") + } + + $keyPath = $line.Substring(0, $eqIndex).Trim() + $valueText = $line.Substring($eqIndex + 1).Trim() + if ($valueText.Length -eq 0) { + throw [System.InvalidOperationException]::new("Missing value for key '$keyPath' on line $($i + 1).") + } + + if ($valueText.StartsWith('"""') -and + ($valueText.Length -lt 6 -or -not $valueText.Substring(3).Contains('"""'))) { + $collector = [System.Text.StringBuilder]::new() + $null = $collector.Append($valueText) + while ($i -lt ($lines.Count - 1)) { + $i++ + $next = $lines[$i] + $null = $collector.Append("`n") + $null = $collector.Append($next) + if ($next.Contains('"""')) { + break + } + } + $valueText = $collector.ToString() + } elseif ($valueText.StartsWith("'''") -and + ($valueText.Length -lt 6 -or -not $valueText.Substring(3).Contains("'''"))) { + $collector = [System.Text.StringBuilder]::new() + $null = $collector.Append($valueText) + while ($i -lt ($lines.Count - 1)) { + $i++ + $next = $lines[$i] + $null = $collector.Append("`n") + $null = $collector.Append($next) + if ($next.Contains("'''")) { + break + } + } + $valueText = $collector.ToString() + } elseif (($valueText.StartsWith('[') -and -not $valueText.EndsWith(']')) -or + ($valueText.StartsWith('{') -and -not $valueText.EndsWith('}'))) { + $collector = [System.Text.StringBuilder]::new() + $null = $collector.Append($valueText) + while ($i -lt ($lines.Count - 1)) { + $i++ + $next = (Get-TomlContentWithoutComment -Line $lines[$i]).Trim() + if ($next.Length -eq 0) { + continue + } + $null = $collector.Append(' ') + $null = $collector.Append($next) + if ($valueText.StartsWith('[') -and $collector.ToString().Trim().EndsWith(']')) { break } + if ($valueText.StartsWith('{') -and $collector.ToString().Trim().EndsWith('}')) { break } + } + $valueText = $collector.ToString() + } + + $keySegments = Split-TomlDottedKey -KeyPath $keyPath + $target = $currentTable + for ($s = 0; $s -lt ($keySegments.Count - 1); $s++) { + $segment = $keySegments[$s] + if (-not $target.Contains($segment)) { + $target[$segment] = [System.Collections.Specialized.OrderedDictionary]::new([System.StringComparer]::Ordinal) + } + if ($target[$segment] -isnot [System.Collections.Specialized.OrderedDictionary]) { + throw [System.InvalidOperationException]::new("Cannot define dotted key '$keyPath' because '$segment' is not a table.") + } + $target = $target[$segment] + } + + $leafKey = $keySegments[-1] + if ($target.Contains($leafKey)) { + throw [System.InvalidOperationException]::new("The key '$keyPath' is already defined and cannot be redefined.") + } + + if ($valueText.Trim() -eq '[]') { + $target[$leafKey] = @() + } else { + $target[$leafKey] = ConvertFrom-TomlValue -Value $valueText + } + } + + return $root +} diff --git a/src/functions/private/Toml/ConvertFrom-TomlValue.ps1 b/src/functions/private/Toml/ConvertFrom-TomlValue.ps1 new file mode 100644 index 0000000..7caeac4 --- /dev/null +++ b/src/functions/private/Toml/ConvertFrom-TomlValue.ps1 @@ -0,0 +1,27 @@ +function ConvertFrom-TomlValue { + <# + .SYNOPSIS + Converts a TOML value token to a native PowerShell value. + #> + [OutputType([object])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [AllowEmptyString()] + [string] $Value + ) + + $text = $Value.Trim() + if ($text.Length -eq 0) { + throw [System.InvalidOperationException]::new('Missing TOML value.') + } + + $index = 0 + $parsed = ConvertFrom-TomlParsedValue -Source $text -Index ([ref]$index) + Skip-TomlWhitespace -Source $text -Index ([ref]$index) + if ($index -lt $text.Length) { + throw [System.InvalidOperationException]::new("Unexpected trailing token in TOML value: '$($text.Substring($index))'.") + } + + return $parsed +} diff --git a/src/functions/private/Toml/ConvertFrom-TomlynTable.ps1 b/src/functions/private/Toml/ConvertFrom-TomlynTable.ps1 deleted file mode 100644 index dccaa19..0000000 --- a/src/functions/private/Toml/ConvertFrom-TomlynTable.ps1 +++ /dev/null @@ -1,43 +0,0 @@ -function ConvertFrom-TomlynTable { - <# - .SYNOPSIS - Converts a Tomlyn TomlTable to an [ordered] hashtable. - - .DESCRIPTION - Iterates every key-value pair in the Tomlyn model TomlTable and - calls ConvertFrom-TomlynValue recursively, building an - [ordered] hashtable that preserves TOML key order. - - .EXAMPLE - $table = [Tomlyn.TomlSerializer]::Deserialize[Tomlyn.Model.TomlTable]($toml, $opts) - ConvertFrom-TomlynTable -Table $table - - Returns an [ordered] hashtable of the table's contents. - - .INPUTS - [Tomlyn.Model.TomlTable] - - .OUTPUTS - [System.Collections.Specialized.OrderedDictionary] - - .NOTES - Internal helper. Not exported. No pipeline input. - #> - [OutputType([System.Collections.Specialized.OrderedDictionary])] - [CmdletBinding()] - param( - # The Tomlyn TomlTable to convert. - [Parameter(Mandatory)] - [Tomlyn.Model.TomlTable] $Table - ) - - $dict = [System.Collections.Specialized.OrderedDictionary]::new( - [System.StringComparer]::Ordinal - ) - - foreach ($kv in $Table) { - $dict[$kv.Key] = ConvertFrom-TomlynValue -Value $kv.Value - } - - return $dict -} diff --git a/src/functions/private/Toml/ConvertFrom-TomlynValue.ps1 b/src/functions/private/Toml/ConvertFrom-TomlynValue.ps1 deleted file mode 100644 index 5f76b42..0000000 --- a/src/functions/private/Toml/ConvertFrom-TomlynValue.ps1 +++ /dev/null @@ -1,74 +0,0 @@ -function ConvertFrom-TomlynValue { - <# - .SYNOPSIS - Converts a Tomlyn model value to a native PowerShell value. - - .DESCRIPTION - Recursively maps the Tomlyn object model (TomlTable, TomlTableArray, - TomlArray, TomlDateTime, and scalars) to corresponding PowerShell types: - - Tomlyn.Model.TomlTable -> [System.Collections.Specialized.OrderedDictionary] - - Tomlyn.Model.TomlTableArray -> [object[]] of [ordered] - - Tomlyn.Model.TomlArray -> [object[]] - - Tomlyn.TomlDateTime -> [DateTimeOffset], [datetime], or [timespan] - - string, bool, long, double -> their PowerShell equivalents - - .EXAMPLE - $table = [Tomlyn.TomlSerializer]::Deserialize[Tomlyn.Model.TomlTable]($toml, $opts) - ConvertFrom-TomlynValue -Value $table - - Returns an [ordered] hashtable representing the TOML table. - - .INPUTS - [object] — any Tomlyn model value. - - .OUTPUTS - [object] - - .NOTES - Internal helper. Not exported. No pipeline input. - #> - [OutputType( - [System.Collections.Specialized.OrderedDictionary], [object[]], - [string], [long], [double], [bool], - [System.DateTimeOffset], [System.DateTime], [System.TimeSpan] - )] - [CmdletBinding()] - param( - # The Tomlyn model value to convert. - [Parameter(Mandatory)] - [AllowNull()] - [object] $Value - ) - - if ($null -eq $Value) { - return $null - } - - if ($Value -is [Tomlyn.Model.TomlTableArray]) { - # Array of tables: [[key]] sections - $list = [System.Collections.Generic.List[object]]::new() - foreach ($tableItem in $Value) { - $list.Add((ConvertFrom-TomlynTable -Table $tableItem)) - } - return , $list.ToArray() - } - - if ($Value -is [Tomlyn.Model.TomlTable]) { - return ConvertFrom-TomlynTable -Table $Value - } - - if ($Value -is [Tomlyn.Model.TomlArray]) { - $list = [System.Collections.Generic.List[object]]::new() - foreach ($item in $Value) { - $list.Add((ConvertFrom-TomlynValue -Value $item)) - } - return , $list.ToArray() - } - - if ($Value -is [Tomlyn.TomlDateTime]) { - return ConvertFrom-TomlDateTime -TomlDt $Value - } - - # Scalar: string, bool, long, double — already a native .NET type - return $Value -} diff --git a/src/functions/private/Toml/ConvertTo-TomlArrayObject.ps1 b/src/functions/private/Toml/ConvertTo-TomlArrayObject.ps1 new file mode 100644 index 0000000..b9860da --- /dev/null +++ b/src/functions/private/Toml/ConvertTo-TomlArrayObject.ps1 @@ -0,0 +1,19 @@ +function ConvertTo-TomlArrayObject { + <# + .SYNOPSIS + Normalizes a PowerShell enumerable to a TOML-compatible array. + #> + [OutputType([object[]])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [System.Collections.IEnumerable] $Value + ) + + $items = [System.Collections.Generic.List[object]]::new() + foreach ($item in $Value) { + $items.Add((ConvertTo-TomlTableObject -Value $item)) + } + + return , $items.ToArray() +} diff --git a/src/functions/private/Toml/ConvertTo-TomlTableObject.ps1 b/src/functions/private/Toml/ConvertTo-TomlTableObject.ps1 new file mode 100644 index 0000000..a36531a --- /dev/null +++ b/src/functions/private/Toml/ConvertTo-TomlTableObject.ps1 @@ -0,0 +1,51 @@ +function ConvertTo-TomlTableObject { + <# + .SYNOPSIS + Normalizes input data to TOML-compatible objects. + #> + [OutputType([object])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [AllowNull()] + [object] $Value + ) + + if ($null -eq $Value) { + throw [System.ArgumentNullException]::new('Value', 'TOML does not support null values.') + } + + if ($Value -is [System.Collections.Specialized.OrderedDictionary]) { + $result = [System.Collections.Specialized.OrderedDictionary]::new([System.StringComparer]::Ordinal) + foreach ($key in $Value.Keys) { + $result[$key] = ConvertTo-TomlTableObject -Value $Value[$key] + } + return $result + } + + if ($Value -is [System.Collections.IDictionary]) { + $result = [System.Collections.Specialized.OrderedDictionary]::new([System.StringComparer]::Ordinal) + foreach ($key in $Value.Keys) { + $result[$key.ToString()] = ConvertTo-TomlTableObject -Value $Value[$key] + } + return $result + } + + if ($Value -is [System.Management.Automation.PSCustomObject] -or + ($Value -is [psobject] -and $Value -isnot [string] -and $Value -isnot [System.Collections.IEnumerable])) { + $result = [System.Collections.Specialized.OrderedDictionary]::new([System.StringComparer]::Ordinal) + foreach ($prop in $Value.PSObject.Properties) { + if ($prop.MemberType -notin 'NoteProperty', 'Property', 'ScriptProperty') { + continue + } + $result[$prop.Name] = ConvertTo-TomlTableObject -Value $prop.Value + } + return $result + } + + if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) { + return ConvertTo-TomlArrayObject -Value $Value + } + + return $Value +} diff --git a/src/functions/private/Toml/ConvertTo-TomlValue.ps1 b/src/functions/private/Toml/ConvertTo-TomlValue.ps1 new file mode 100644 index 0000000..2eb0578 --- /dev/null +++ b/src/functions/private/Toml/ConvertTo-TomlValue.ps1 @@ -0,0 +1,83 @@ +function ConvertTo-TomlValue { + <# + .SYNOPSIS + Converts a normalized value to a TOML literal. + #> + [OutputType([string])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [AllowNull()] + [object] $Value + ) + + if ($null -eq $Value) { + throw [System.InvalidOperationException]::new('TOML does not support null values.') + } + + $culture = [System.Globalization.CultureInfo]::InvariantCulture + + if ($Value -is [string]) { + $escaped = $Value.Replace('\', '\\').Replace('"', '\"').Replace("`t", '\t').Replace("`r", '\r').Replace("`n", '\n') + return '"' + $escaped + '"' + } + + if ($Value -is [bool]) { + if ($Value) { return 'true' } + return 'false' + } + + if ($Value -is [byte] -or $Value -is [sbyte] -or $Value -is [int16] -or + $Value -is [uint16] -or $Value -is [int32] -or $Value -is [uint32] -or + $Value -is [int64] -or $Value -is [uint64]) { + return [string]::Format($culture, '{0}', $Value) + } + + if ($Value -is [single] -or $Value -is [double] -or $Value -is [decimal]) { + $d = [double]$Value + if ([double]::IsNaN($d)) { return 'nan' } + if ([double]::IsPositiveInfinity($d)) { return 'inf' } + if ([double]::IsNegativeInfinity($d)) { return '-inf' } + return $d.ToString('R', $culture) + } + + if ($Value -is [System.DateTimeOffset]) { + return $Value.ToString('yyyy-MM-ddTHH:mm:ssK', $culture) + } + + if ($Value -is [System.DateTime]) { + $dt = [System.DateTime]::SpecifyKind($Value, [System.DateTimeKind]::Unspecified) + if ($dt.TimeOfDay -eq [System.TimeSpan]::Zero) { + return $dt.ToString('yyyy-MM-dd', $culture) + } + return $dt.ToString('yyyy-MM-ddTHH:mm:ss', $culture) + } + + if ($Value -is [System.TimeSpan]) { + $base = '{0:00}:{1:00}:{2:00}' -f $Value.Hours, $Value.Minutes, $Value.Seconds + if ($Value.Milliseconds -gt 0) { + return $base + '.' + $Value.Milliseconds.ToString('000', $culture) + } + return $base + } + + if ($Value -is [object[]]) { + $parts = [System.Collections.Generic.List[string]]::new() + foreach ($item in $Value) { + $parts.Add((ConvertTo-TomlValue -Value $item)) + } + return '[ ' + ($parts -join ', ') + ' ]' + } + + if ($Value -is [System.Collections.Specialized.OrderedDictionary]) { + $parts = [System.Collections.Generic.List[string]]::new() + foreach ($key in $Value.Keys) { + $parts.Add("$(Format-TomlKey -Key $key) = $(ConvertTo-TomlValue -Value $Value[$key])") + } + return '{ ' + ($parts -join ', ') + ' }' + } + + throw [System.InvalidOperationException]::new( + "Cannot serialize value of type '$($Value.GetType().FullName)' to TOML." + ) +} diff --git a/src/functions/private/Toml/ConvertTo-TomlynArray.ps1 b/src/functions/private/Toml/ConvertTo-TomlynArray.ps1 deleted file mode 100644 index 68931f2..0000000 --- a/src/functions/private/Toml/ConvertTo-TomlynArray.ps1 +++ /dev/null @@ -1,41 +0,0 @@ -function ConvertTo-TomlynArray { - <# - .SYNOPSIS - Converts a PowerShell enumerable to a Tomlyn TomlArray. - - .DESCRIPTION - Iterates each element of the input collection and calls ConvertTo-TomlynValue - recursively. Strings are enumerated character-by-character by default in - PowerShell, so strings are handled explicitly before the IEnumerable path. - - .EXAMPLE - ConvertTo-TomlynArray -Value @(1, 2, 3) - - Returns a [Tomlyn.Model.TomlArray] with three integer elements. - - .INPUTS - [System.Collections.IEnumerable] - - .OUTPUTS - [Tomlyn.Model.TomlArray] - - .NOTES - Internal helper. Not exported. No pipeline input. - #> - [OutputType([Tomlyn.Model.TomlArray])] - [CmdletBinding()] - param( - # The collection to convert. - [Parameter(Mandatory)] - [System.Collections.IEnumerable] $Value - ) - - $array = [Tomlyn.Model.TomlArray]::new() - - foreach ($item in $Value) { - $array.Add((ConvertTo-TomlynValue -Value $item)) - } - - # Wrap to prevent PowerShell from enumerating TomlArray (IEnumerable) - return , $array -} diff --git a/src/functions/private/Toml/ConvertTo-TomlynTable.ps1 b/src/functions/private/Toml/ConvertTo-TomlynTable.ps1 deleted file mode 100644 index c49d7ff..0000000 --- a/src/functions/private/Toml/ConvertTo-TomlynTable.ps1 +++ /dev/null @@ -1,55 +0,0 @@ -function ConvertTo-TomlynTable { - <# - .SYNOPSIS - Converts a PowerShell dictionary or PSCustomObject to a Tomlyn TomlTable. - - .DESCRIPTION - Iterates the properties/keys of the input and calls ConvertTo-TomlynValue - recursively for each value. Accepts [hashtable], [ordered], - [System.Collections.Specialized.OrderedDictionary], and [PSCustomObject]. - - .EXAMPLE - ConvertTo-TomlynTable -Value @{ host = 'localhost'; port = 5432 } - - Returns a [Tomlyn.Model.TomlTable] with two entries. - - .INPUTS - [object] - - .OUTPUTS - [Tomlyn.Model.TomlTable] - - .NOTES - Internal helper. Not exported. No pipeline input. - #> - [OutputType([Tomlyn.Model.TomlTable])] - [CmdletBinding()] - param( - # The dictionary or PSCustomObject to convert. - [Parameter(Mandatory)] - [object] $Value - ) - - $table = [Tomlyn.Model.TomlTable]::new() - - if ($Value -is [System.Management.Automation.PSCustomObject]) { - foreach ($prop in $Value.PSObject.Properties) { - if ($prop.MemberType -notin 'NoteProperty', 'ScriptProperty', 'Property') { - continue - } - $table[$prop.Name] = ConvertTo-TomlynValue -Value $prop.Value - } - } elseif ($Value -is [System.Collections.IDictionary]) { - foreach ($key in $Value.Keys) { - $table[$key.ToString()] = ConvertTo-TomlynValue -Value $Value[$key] - } - } else { - throw [System.InvalidOperationException]::new( - "Cannot convert '$($Value.GetType().FullName)' to a TOML table. " + - 'Provide a [hashtable], [ordered], or [PSCustomObject].' - ) - } - - # Wrap in array to prevent PowerShell from enumerating TomlTable (which implements IEnumerable) - return , $table -} diff --git a/src/functions/private/Toml/ConvertTo-TomlynValue.ps1 b/src/functions/private/Toml/ConvertTo-TomlynValue.ps1 deleted file mode 100644 index b89ccf2..0000000 --- a/src/functions/private/Toml/ConvertTo-TomlynValue.ps1 +++ /dev/null @@ -1,128 +0,0 @@ -function ConvertTo-TomlynValue { - <# - .SYNOPSIS - Converts a PowerShell value to the appropriate Tomlyn model object. - - .DESCRIPTION - Maps PowerShell types to Tomlyn model objects used by TomlSerializer: - - [System.DateTimeOffset] -> Tomlyn.TomlDateTime (OffsetDateTimeByNumber) - - [System.DateTime] (date-only = midnight) -> Tomlyn.TomlDateTime (LocalDate) - - [System.DateTime] (has time component) -> Tomlyn.TomlDateTime (LocalDateTime) - - [System.TimeSpan] -> Tomlyn.TomlDateTime (LocalTime) - - [hashtable] / [OrderedDictionary] / [PSCustomObject] / [PSObject] -> TomlTable - - [array] / [List] / [IEnumerable] -> TomlArray - - [bool] -> bool - - [int16/32/64] / [byte] / [sbyte] -> long - - [single/double] -> double - - [string] -> string - - everything else -> string via ToString() - - .EXAMPLE - ConvertTo-TomlynValue -Value 42 - Returns [long] 42. - - .EXAMPLE - ConvertTo-TomlynValue -Value @{ key = 'val' } - Returns a [Tomlyn.Model.TomlTable]. - - .INPUTS - [object] - - .OUTPUTS - [object] - - .NOTES - Internal helper. Not exported. No pipeline input. - #> - [OutputType([object])] - [CmdletBinding()] - param( - # The PowerShell value to convert. - [Parameter(Mandatory)] - [AllowNull()] - [object] $Value - ) - - if ($null -eq $Value) { - throw [System.ArgumentNullException]::new( - 'Value', - 'TOML does not support null values. Remove the key or supply a valid value.' - ) - } - - # Unwrap PSObject wrapper - if ($Value -is [System.Management.Automation.PSObject] -and - $Value -isnot [System.Management.Automation.PSCustomObject]) { - $Value = $Value.BaseObject - } - - if ($Value -is [System.DateTimeOffset]) { - return [Tomlyn.TomlDateTime]::new( - $Value, - 0, - [Tomlyn.TomlDateTimeKind]::OffsetDateTimeByNumber - ) - } - - if ($Value -is [System.DateTime]) { - if ($Value.TimeOfDay -eq [System.TimeSpan]::Zero) { - # Date-only: use the (year, month, day) constructor for LocalDate - return [Tomlyn.TomlDateTime]::new($Value.Year, $Value.Month, $Value.Day) - } else { - # Date + time: use (DateTime) constructor for LocalDateTime - return [Tomlyn.TomlDateTime]::new( - [System.DateTime]::SpecifyKind($Value, [System.DateTimeKind]::Unspecified) - ) - } - } - - if ($Value -is [System.TimeSpan]) { - # LocalTime — build a DateTimeOffset on epoch with the time component - $dto = [System.DateTimeOffset]::new( - 1970, 1, 1, - $Value.Hours, $Value.Minutes, $Value.Seconds, - [System.TimeSpan]::Zero - ) - return [Tomlyn.TomlDateTime]::new($dto, 0, [Tomlyn.TomlDateTimeKind]::LocalTime) - } - - if ($Value -is [bool]) { - return $Value - } - - if ($Value -is [System.Int16] -or - $Value -is [System.Int32] -or - $Value -is [System.Int64] -or - $Value -is [System.Byte] -or - $Value -is [System.SByte] -or - $Value -is [System.UInt16] -or - $Value -is [System.UInt32] -or - $Value -is [System.UInt64]) { - return [long]$Value - } - - if ($Value -is [System.Single] -or $Value -is [System.Double]) { - return [double]$Value - } - - if ($Value -is [string]) { - return $Value - } - - if ($Value -is [System.Collections.IDictionary] -or - $Value -is [System.Management.Automation.PSCustomObject]) { - $subTable = ConvertTo-TomlynTable -Value $Value - # Use , to prevent PowerShell from iterating the TomlTable (IEnumerable) in the pipeline - return , $subTable - } - - # Array and list types - if ($Value -is [System.Collections.IEnumerable]) { - $subArray = ConvertTo-TomlynArray -Value $Value - # Use , to prevent PowerShell from iterating the TomlArray (IEnumerable) in the pipeline - return , $subArray - } - - # Fallback: stringify - return $Value.ToString() -} diff --git a/src/functions/private/Toml/Format-TomlKey.ps1 b/src/functions/private/Toml/Format-TomlKey.ps1 new file mode 100644 index 0000000..727368b --- /dev/null +++ b/src/functions/private/Toml/Format-TomlKey.ps1 @@ -0,0 +1,19 @@ +function Format-TomlKey { + <# + .SYNOPSIS + Formats a key for TOML output. + #> + [OutputType([string])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Key + ) + + if ($Key -match '^[A-Za-z0-9_-]+$') { + return $Key + } + + $escaped = $Key.Replace('\', '\\').Replace('"', '\"') + return '"' + $escaped + '"' +} diff --git a/src/functions/private/Toml/Get-TomlBareToken.ps1 b/src/functions/private/Toml/Get-TomlBareToken.ps1 new file mode 100644 index 0000000..539d527 --- /dev/null +++ b/src/functions/private/Toml/Get-TomlBareToken.ps1 @@ -0,0 +1,32 @@ +function Get-TomlBareToken { + <# + .SYNOPSIS + Reads a bare TOML token from source. + #> + [OutputType([string])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Source, + + [Parameter(Mandatory)] + [ref] $Index, + + [Parameter()] + [bool] $StopAtEquals = $false + ) + + $start = $Index.Value + while ($Index.Value -lt $Source.Length) { + $ch = $Source[$Index.Value] + if ($ch -in ',', ']', '}') { + break + } + if ($StopAtEquals -and $ch -eq '=') { + break + } + $Index.Value++ + } + + return $Source.Substring($start, $Index.Value - $start).Trim() +} diff --git a/src/functions/private/Toml/Get-TomlContentWithoutComment.ps1 b/src/functions/private/Toml/Get-TomlContentWithoutComment.ps1 new file mode 100644 index 0000000..20e7b30 --- /dev/null +++ b/src/functions/private/Toml/Get-TomlContentWithoutComment.ps1 @@ -0,0 +1,64 @@ +function Get-TomlContentWithoutComment { + <# + .SYNOPSIS + Removes TOML inline comments while preserving quoted text. + #> + [OutputType([string])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [AllowEmptyString()] + [string] $Line + ) + + $sb = [System.Text.StringBuilder]::new() + $inBasic = $false + $inLiteral = $false + $escape = $false + for ($i = 0; $i -lt $Line.Length; $i++) { + $ch = $Line[$i] + + if ($escape) { + $null = $sb.Append($ch) + $escape = $false + continue + } + + if ($inBasic) { + $null = $sb.Append($ch) + if ($ch -eq '\') { + $escape = $true + } elseif ($ch -eq '"') { + $inBasic = $false + } + continue + } + + if ($inLiteral) { + $null = $sb.Append($ch) + if ($ch -eq '''') { + $inLiteral = $false + } + continue + } + + if ($ch -eq '"') { + $inBasic = $true + $null = $sb.Append($ch) + continue + } + if ($ch -eq '''') { + $inLiteral = $true + $null = $sb.Append($ch) + continue + } + + if ($ch -eq '#') { + break + } + + $null = $sb.Append($ch) + } + + return $sb.ToString() +} diff --git a/src/functions/private/Toml/Get-TomlNestedTable.ps1 b/src/functions/private/Toml/Get-TomlNestedTable.ps1 new file mode 100644 index 0000000..78c5e30 --- /dev/null +++ b/src/functions/private/Toml/Get-TomlNestedTable.ps1 @@ -0,0 +1,48 @@ +function Get-TomlNestedTable { + <# + .SYNOPSIS + Resolves or creates nested table path segments. + #> + [OutputType([System.Collections.Specialized.OrderedDictionary])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [System.Collections.Specialized.OrderedDictionary] $StartTable, + + [Parameter()] + [AllowEmptyCollection()] + [string[]] $Segments, + + [Parameter(Mandatory)] + [hashtable] $ArrayLastTableByPath + ) + + $table = $StartTable + $pathParts = [System.Collections.Generic.List[string]]::new() + foreach ($segment in $Segments) { + $pathParts.Add($segment) + $path = Join-TomlKeyPath -Segments $pathParts.ToArray() + + if (-not $table.Contains($segment)) { + $table[$segment] = [System.Collections.Specialized.OrderedDictionary]::new([System.StringComparer]::Ordinal) + } + + $candidate = $table[$segment] + if ($candidate -is [System.Collections.ArrayList]) { + if ($candidate.Count -eq 0) { + throw [System.InvalidOperationException]::new("Array-of-tables '$path' has no current item.") + } + $table = $candidate[-1] + $ArrayLastTableByPath[$path] = $table + continue + } + + if ($candidate -isnot [System.Collections.Specialized.OrderedDictionary]) { + throw [System.InvalidOperationException]::new("Cannot define table '$path' because '$segment' is already a non-table value.") + } + + $table = $candidate + } + + return $table +} diff --git a/src/functions/private/Toml/Join-TomlKeyPath.ps1 b/src/functions/private/Toml/Join-TomlKeyPath.ps1 new file mode 100644 index 0000000..bf1d982 --- /dev/null +++ b/src/functions/private/Toml/Join-TomlKeyPath.ps1 @@ -0,0 +1,19 @@ +function Join-TomlKeyPath { + <# + .SYNOPSIS + Joins key path segments into a dotted path. + #> + [OutputType([string])] + [CmdletBinding()] + param( + [Parameter()] + [AllowEmptyCollection()] + [string[]] $Segments + ) + + if ($null -eq $Segments) { + return '' + } + + return ($Segments -join '.') +} diff --git a/src/functions/private/Toml/Skip-TomlWhitespace.ps1 b/src/functions/private/Toml/Skip-TomlWhitespace.ps1 new file mode 100644 index 0000000..0d681e1 --- /dev/null +++ b/src/functions/private/Toml/Skip-TomlWhitespace.ps1 @@ -0,0 +1,18 @@ +function Skip-TomlWhitespace { + <# + .SYNOPSIS + Advances an index past whitespace in a TOML source string. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Source, + + [Parameter(Mandatory)] + [ref] $Index + ) + + while ($Index.Value -lt $Source.Length -and [char]::IsWhiteSpace($Source[$Index.Value])) { + $Index.Value++ + } +} diff --git a/src/functions/private/Toml/Split-TomlDottedKey.ps1 b/src/functions/private/Toml/Split-TomlDottedKey.ps1 new file mode 100644 index 0000000..3a6d49e --- /dev/null +++ b/src/functions/private/Toml/Split-TomlDottedKey.ps1 @@ -0,0 +1,86 @@ +function Split-TomlDottedKey { + <# + .SYNOPSIS + Splits a TOML dotted key into normalized key segments. + #> + [OutputType([string[]])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $KeyPath + ) + + $segments = [System.Collections.Generic.List[string]]::new() + $sb = [System.Text.StringBuilder]::new() + $inBasic = $false + $inLiteral = $false + $escape = $false + + for ($i = 0; $i -lt $KeyPath.Length; $i++) { + $ch = $KeyPath[$i] + if ($escape) { + $null = $sb.Append($ch) + $escape = $false + continue + } + + if ($inBasic) { + $null = $sb.Append($ch) + if ($ch -eq '\') { + $escape = $true + } elseif ($ch -eq '"') { + $inBasic = $false + } + continue + } + + if ($inLiteral) { + $null = $sb.Append($ch) + if ($ch -eq '''') { + $inLiteral = $false + } + continue + } + + if ($ch -eq '"') { + $inBasic = $true + $null = $sb.Append($ch) + continue + } + if ($ch -eq '''') { + $inLiteral = $true + $null = $sb.Append($ch) + continue + } + + if ($ch -eq '.') { + $segments.Add($sb.ToString().Trim()) + $null = $sb.Clear() + continue + } + + $null = $sb.Append($ch) + } + + $segments.Add($sb.ToString().Trim()) + $normalized = [System.Collections.Generic.List[string]]::new() + foreach ($segment in $segments) { + if ($segment.Length -eq 0) { + throw [System.InvalidOperationException]::new("Invalid dotted key path '$KeyPath'.") + } + + if ($segment.StartsWith('"') -and $segment.EndsWith('"')) { + $normalized.Add((ConvertFrom-TomlValue -Value $segment)) + continue + } + + if ($segment.StartsWith("'") -and $segment.EndsWith("'")) { + $normalized.Add((ConvertFrom-TomlValue -Value $segment)) + continue + } + + $normalized.Add($segment) + } + + return , $normalized.ToArray() +} diff --git a/src/functions/private/Toml/Test-TomlEndsWithDoubleNewLine.ps1 b/src/functions/private/Toml/Test-TomlEndsWithDoubleNewLine.ps1 new file mode 100644 index 0000000..b6c5fb6 --- /dev/null +++ b/src/functions/private/Toml/Test-TomlEndsWithDoubleNewLine.ps1 @@ -0,0 +1,20 @@ +function Test-TomlEndsWithDoubleNewLine { + <# + .SYNOPSIS + Tests whether a StringBuilder ends with two LF characters. + #> + [OutputType([bool])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [System.Text.StringBuilder] $StringBuilder + ) + + if ($StringBuilder.Length -lt 2) { + return $false + } + + $last = $StringBuilder[$StringBuilder.Length - 1] + $prev = $StringBuilder[$StringBuilder.Length - 2] + return ($prev -eq "`n" -and $last -eq "`n") +} diff --git a/src/functions/private/Toml/Test-TomlTableArray.ps1 b/src/functions/private/Toml/Test-TomlTableArray.ps1 new file mode 100644 index 0000000..e2c0c1f --- /dev/null +++ b/src/functions/private/Toml/Test-TomlTableArray.ps1 @@ -0,0 +1,38 @@ +function Test-TomlTableArray { + <# + .SYNOPSIS + Tests whether a value is an array of TOML tables. + #> + [OutputType([bool])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [object] $Value + ) + + if ($null -eq $Value) { + return $false + } + + if ($Value -is [string]) { + return $false + } + + if ($Value -is [System.Collections.IDictionary]) { + return $false + } + + if ($Value -isnot [System.Collections.IEnumerable]) { + return $false + } + + $hasAny = $false + foreach ($item in $Value) { + $hasAny = $true + if ($item -isnot [System.Collections.Specialized.OrderedDictionary]) { + return $false + } + } + + return $hasAny +} diff --git a/src/functions/public/Toml/ConvertFrom-Toml.ps1 b/src/functions/public/Toml/ConvertFrom-Toml.ps1 index da96dfb..5ab984a 100644 --- a/src/functions/public/Toml/ConvertFrom-Toml.ps1 +++ b/src/functions/public/Toml/ConvertFrom-Toml.ps1 @@ -1,101 +1,29 @@ function ConvertFrom-Toml { <# .SYNOPSIS - Converts a TOML string to a TomlDocument object. + Converts TOML text to a TomlDocument. .DESCRIPTION - Parses a TOML-formatted string using the Tomlyn library and returns a - TomlDocument whose Data property contains an ordered dictionary of the - document's key-value pairs. TOML tables become nested ordered dictionaries, - arrays become object arrays, and TOML scalar types are mapped to PowerShell - types as follows: - - | TOML type | PowerShell type | - |---------------------|--------------------------| - | String | [string] | - | Integer | [long] | - | Float | [double] | - | Boolean | [bool] | - | Offset date-time | [System.DateTimeOffset] | - | Local date-time | [System.DateTime] | - | Local date | [System.DateTime] | - | Local time | [System.TimeSpan] | - | Array | [object[]] | - | Table / Inline table| [ordered] hashtable | - - Full TOML 1.0.0 specification is supported. - - .EXAMPLE - $doc = ConvertFrom-Toml -InputObject '[database] - host = "localhost" - port = 5432' - - $doc.Data.database.host # returns 'localhost' - $doc.Data.database.port # returns 5432 - - Parses a simple TOML document with a table and two keys. - - .EXAMPLE - $toml = Get-Content 'config.toml' -Raw - $config = ConvertFrom-Toml -InputObject $toml - $config.Data.server.timeout - - Reads a TOML file and accesses a nested value. - - .INPUTS - [string] - - .OUTPUTS - [TomlDocument] - - .NOTES - Throws [Tomlyn.TomlException] or [System.InvalidOperationException] on - invalid TOML input. Error messages include line and column information - from the Tomlyn parser. + Parses TOML-formatted text into a TomlDocument object with OrderedDictionary + semantics and TOML-compatible scalar mappings. #> [OutputType([TomlDocument])] [CmdletBinding()] param( - # The TOML-formatted string to parse. Must be valid TOML 1.0.0. [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string] $InputObject ) process { - $opts = [Tomlyn.TomlSerializerOptions]::new() - $tomlTable = $null - try { - # SyntaxParser.ParseStrict validates the full document and throws a - # detailed exception on duplicate keys or table redefinition, which - # TomlSerializer.Deserialize does not enforce by itself. - $null = [Tomlyn.Parsing.SyntaxParser]::ParseStrict($InputObject, $opts, $null, $true) + $data = ConvertFrom-TomlTable -InputObject $InputObject + return [TomlDocument]::new($data) } catch { throw [System.InvalidOperationException]::new( "Failed to parse TOML: $($_.Exception.Message)", $_.Exception ) } - - try { - $tomlTable = [Tomlyn.TomlSerializer]::Deserialize[Tomlyn.Model.TomlTable]( - $InputObject, - $opts - ) - } catch [Tomlyn.TomlException] { - throw [System.InvalidOperationException]::new( - "Failed to parse TOML: $($_.Exception.Message)", - $_.Exception - ) - } catch { - throw [System.InvalidOperationException]::new( - "Unexpected error parsing TOML: $($_.Exception.Message)", - $_.Exception - ) - } - - $data = ConvertFrom-TomlynTable -Table $tomlTable - [TomlDocument]::new($data) } } diff --git a/src/functions/public/Toml/ConvertTo-Toml.ps1 b/src/functions/public/Toml/ConvertTo-Toml.ps1 index 4739b4e..348ca2d 100644 --- a/src/functions/public/Toml/ConvertTo-Toml.ps1 +++ b/src/functions/public/Toml/ConvertTo-Toml.ps1 @@ -1,97 +1,32 @@ function ConvertTo-Toml { <# .SYNOPSIS - Converts a PowerShell object graph to a TOML string. - - .DESCRIPTION - Serializes a [TomlDocument], [hashtable], [ordered] dictionary, or - [PSCustomObject] into a TOML-formatted string using the Tomlyn library. - - Accepted input types and their TOML encoding: - - [string] -> TOML String - - [bool] -> TOML Boolean (true / false) - - [int16/32/64] / [byte] -> TOML Integer - - [single] / [double] -> TOML Float - - [System.DateTimeOffset] -> TOML Offset date-time - - [System.DateTime] (no time component) -> TOML Local date - - [System.DateTime] (has time) -> TOML Local date-time - - [System.TimeSpan] -> TOML Local time - - [hashtable] / [ordered] / [PSCustomObject] -> TOML Table - - [array] / [List] / IEnumerable -> TOML Array - - [TomlDocument] -> TOML document from .Data - - Null values are not supported by TOML and cause a terminating error. - - .EXAMPLE - ConvertTo-Toml -InputObject ([ordered]@{ - title = 'Config' - server = [ordered]@{ host = 'localhost'; port = 8080 } - }) - - Produces: - title = "Config" - [server] - host = "localhost" - port = 8080 - - .EXAMPLE - $doc = ConvertFrom-Toml -InputObject $tomlString - ConvertTo-Toml -InputObject $doc - - Round-trips a parsed TOML document back to a TOML string. - - .INPUTS - [object] - - .OUTPUTS - [string] - - .NOTES - The output format follows Tomlyn's default serialization, which produces - valid TOML 1.0.0. Key order is preserved when the input is an [ordered] - dictionary or [TomlDocument]. + Converts a PowerShell object graph to TOML text. #> [OutputType([string])] [CmdletBinding()] param( - # The object to serialize to TOML. Accepts TomlDocument, hashtable, - # ordered dictionary, or PSCustomObject. [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNull()] [object] $InputObject ) process { - # Unwrap TomlDocument to its data dict $source = if ($InputObject -is [TomlDocument]) { $InputObject.Data } else { $InputObject } - $tomlTable = $null - try { - $tomlTable = ConvertTo-TomlynTable -Value $source - } catch [System.ArgumentNullException] { - throw [System.InvalidOperationException]::new( - "Cannot serialize object: $($_.Exception.Message)", - $_.Exception - ) - } catch { + $root = ConvertTo-TomlTableObject -Value $source + if ($root -isnot [System.Collections.Specialized.OrderedDictionary]) { throw [System.InvalidOperationException]::new( - "Failed to build TOML model: $($_.Exception.Message)", - $_.Exception + 'TOML documents must be dictionaries/objects at the root.' ) } - $opts = [Tomlyn.TomlSerializerOptions]::new() - try { - [Tomlyn.TomlSerializer]::Serialize[Tomlyn.Model.TomlTable]($tomlTable, $opts) - } catch { - throw [System.InvalidOperationException]::new( - "Failed to serialize TOML: $($_.Exception.Message)", - $_.Exception - ) - } + $sb = [System.Text.StringBuilder]::new() + Add-TomlTableText -StringBuilder $sb -Table $root -Path '' -EmitHeader:$false + return $sb.ToString().TrimEnd() } } diff --git a/src/init/initializer.ps1 b/src/init/initializer.ps1 index c3cf3f5..42703bd 100644 --- a/src/init/initializer.ps1 +++ b/src/init/initializer.ps1 @@ -1,7 +1 @@ -$assemblyPath = Join-Path -Path $PSScriptRoot -ChildPath '..\assemblies\Tomlyn.dll' -$resolvedPath = [System.IO.Path]::GetFullPath($assemblyPath) - -if (-not [System.AppDomain]::CurrentDomain.GetAssemblies().Where({ $_.GetName().Name -eq 'Tomlyn' })) { - [System.Reflection.Assembly]::LoadFrom($resolvedPath) | Out-Null - Write-Verbose "Loaded Tomlyn assembly from: $resolvedPath" -} +Write-Verbose 'Toml module initialized without external TOML assembly dependencies.' diff --git a/src/manifest.psd1 b/src/manifest.psd1 index ff720bd..27ca5e1 100644 --- a/src/manifest.psd1 +++ b/src/manifest.psd1 @@ -1,5 +1,6 @@ # This file always wins! # Use this file to override any of the framework defaults and generated values. @{ - ModuleVersion = '0.0.0' + ModuleVersion = '0.0.0' + PowerShellVersion = '7.6' } diff --git a/tests/data/advanced/app-config.toml b/tests/data/advanced/app-config.toml new file mode 100644 index 0000000..491caab --- /dev/null +++ b/tests/data/advanced/app-config.toml @@ -0,0 +1,117 @@ +# app-config.toml +# Production application configuration — TOML 1.0.0 +# Features: multi-line basic/literal strings, all escape sequences, Unicode, +# dotted keys, inline tables, arrays, standard/dotted table headers, +# array-of-tables, integer underscores, comments. + +[app] +name = "Web Service" +version = "2.4.1" +debug = false +workers = 4 + +app.build.commit = "a3f9e12b" +app.build.date = 2024-07-23T14:00:00Z +app.build.tag = "v2.4.1" + +# ── String types ────────────────────────────────────────────────────────────── + +[strings] +tabs-and-newlines = "column1\tcolumn2\nrow2-col1\trow2-col2" +quote-in-string = "She said \"hello\" and left." +backslash = "C:\\Users\\Alice\\Documents" +null-byte = "before\u0000after" +smiley = "\U0001F600" +greek-delta = "\u03B4" +snowman = "\u2603" + +sql-query = """ + SELECT u.id, u.name, u.email, \ + p.plan, p.expires_at + FROM users u + JOIN plans p ON p.user_id = u.id + WHERE u.active = TRUE + AND p.expires_at > NOW() + ORDER BY u.name ASC + LIMIT 100;""" + +changelog-entry = """ +## v2.4.1 — 2024-07-23 + +### Fixed +- Memory leak in connection pool +- Race condition in cache eviction +""" + +regex-pattern = '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}' +windows-path-raw = 'C:\Program Files\MyApp\config.ini' + +shell-script = ''' +#!/usr/bin/env bash +set -euo pipefail +echo "Done at $(date)" +''' + +# ── Server ──────────────────────────────────────────────────────────────────── + +[server] +host = "0.0.0.0" +port = 8_080 +read-timeout = 30 +write-timeout = 60 +idle-timeout = 120 + +[server.tls] +enabled = true +cert-file = "/etc/ssl/certs/app.crt" +key-file = "/etc/ssl/private/app.key" +min-version = "TLS1.2" +ciphers = [ + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + "ECDHE-RSA-AES256-GCM-SHA384", +] + +[server.cors] +allowed-origins = ["https://example.com", "https://app.example.com"] +allowed-methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"] +max-age = 86_400 + +# ── Database ────────────────────────────────────────────────────────────────── + +[database] +driver = "postgres" +pool = { min = 5, max = 50, timeout = 30, idle-timeout = 600 } + +# ── Feature flags ───────────────────────────────────────────────────────────── + +[[feature_flag]] +name = "new-dashboard" +enabled = true +rollout-pct = 25 +started = 2024-05-01 + + [feature_flag.targeting] + segments = ["beta-users", "internal"] + regions = ["us-east-1", "eu-west-1"] + +[[feature_flag]] +name = "graphql-api" +enabled = false +rollout-pct = 0 +started = 2024-07-01T00:00:00Z + + [feature_flag.targeting] + segments = [] + regions = ["us-east-1"] + +[[feature_flag]] +name = "ml-recommendations" +enabled = true +rollout-pct = 100 +started = 2024-03-15T09:00:00+02:00 + + [feature_flag.targeting] + segments = ["all"] + regions = ["us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1"] diff --git a/tests/data/advanced/cargo-manifest.toml b/tests/data/advanced/cargo-manifest.toml new file mode 100644 index 0000000..45c077b --- /dev/null +++ b/tests/data/advanced/cargo-manifest.toml @@ -0,0 +1,95 @@ +# Cargo.toml — Rust-style package manifest — TOML 1.0.0 showcase +# Features: dotted keys, multi-line literal strings, all integer forms, +# dotted table paths, array of tables, inline tables, comments. + +[package] +name = "my-awesome-lib" +version = "0.9.1" +edition = "2021" +authors = ["Alice ", "Bob "] +license = "MIT OR Apache-2.0" +description = "A library demonstrating TOML 1.0.0 features." +readme = "README.md" + +package.metadata.docs_rs.features = ["full"] +package.metadata.docs_rs.targets = ["x86_64-unknown-linux-gnu"] + +package.metadata.build_notes = ''' +Build with: + cargo build --release + +Windows cross-compile: + cargo build --target x86_64-pc-windows-gnu \ + --features "tls-native" +No escapes needed: C:\Users\Alice\project is literal. +''' + +[features] +default = ["std", "derive"] +full = ["std", "derive", "async", "serde-support"] +std = [] +derive = [] +async = [] +serde-support = [] + +[lib] +name = "my_awesome_lib" +crate-type = ["rlib", "cdylib"] + +[package.limits] +max-size-decimal = 1_048_576 +max-size-hex = 0x0010_0000 +max-size-octal = 0o4_000_000 +max-size-binary = 0b0001_0000_0000_0000_0000_0000 +page-size = 0o10000 +flag-bits = 0b1100_0011 +magic-number = 0xDEAD_BEEF + +[dependencies] +serde = { version = "1", features = ["derive"] } +tokio = { version = "1", features = ["full"], optional = true } +thiserror = "1" +tracing = "0.1" + +[dependencies.reqwest] +version = "0.12" +default-features = false +features = ["json", "rustls-tls"] +optional = true + +[dev-dependencies] +criterion = { version = "0.5", features = ["html_reports"] } +proptest = "1" +tempfile = "3" + +[[bench]] +name = "encode_throughput" +harness = false + +[[bench]] +name = "decode_throughput" +harness = false + +[[bench]] +name = "roundtrip" +harness = false + +[[example]] +name = "basic_usage" +required-features = ["std"] + +[[example]] +name = "async_client" +required-features = ["async", "serde-support"] + +[profile.release] +opt-level = 3 +lto = "thin" +codegen-units = 1 +strip = "symbols" +overflow-checks = false + +[profile.dev] +opt-level = 0 +debug = true +overflow-checks = true diff --git a/tests/data/advanced/ci-pipeline.toml b/tests/data/advanced/ci-pipeline.toml new file mode 100644 index 0000000..d402660 --- /dev/null +++ b/tests/data/advanced/ci-pipeline.toml @@ -0,0 +1,78 @@ +# ci-pipeline.toml +# Continuous integration pipeline configuration — TOML 1.0.0 +# Features: dotted keys, standard/dotted table headers, array-of-tables, +# multi-level nesting, inline tables, multi-line basic strings, +# integer underscore separators, comments alongside keys. + +[pipeline] # standard table header +name = "my-app-ci" +version = 2 +timeout = 3_600 # integer with underscore separator +fail-fast = true + +# Dotted keys build the "pipeline.meta" sub-table without a [pipeline.meta] header +pipeline.meta.owner = "platform-team" +pipeline.meta.created = 2024-03-15 +pipeline.meta.description = """ +A multi-stage pipeline that builds, \ +tests, and deploys the application \ +to all target environments.""" + +[pipeline.triggers] +branches = ["main", "release/*", "hotfix/*"] +tags = ["v[0-9]*"] +on-pr = true + +[[pipeline.stages]] +name = "build" +image = "rust:1.78-slim" +allow-fail = false + + [pipeline.stages.env] + RUST_BACKTRACE = "1" + CARGO_TERM_COLOR = "always" + + [[pipeline.stages.steps]] + name = "restore-cache" + run = "cargo fetch --locked" + + [[pipeline.stages.steps]] + name = "compile" + run = "cargo build --release --locked" + timeout = 1_800 + +[[pipeline.stages]] +name = "test" +image = "rust:1.78-slim" +depends-on = ["build"] +allow-fail = false + + [pipeline.stages.env] + RUST_LOG = "debug" + + [[pipeline.stages.steps]] + name = "unit-tests" + run = "cargo test --workspace" + + [[pipeline.stages.steps]] + name = "coverage" + run = "cargo llvm-cov --lcov --output-path lcov.info" + timeout = 600 + +[[pipeline.stages]] +name = "deploy" +image = "alpine:3.19" +depends-on = ["build", "test"] +when = { branch = "main", event = "push" } + + [[pipeline.stages.steps]] + name = "upload-artifact" + run = "aws s3 cp ./target/release/my-app s3://releases/" + + [[pipeline.stages.steps]] + name = "notify-slack" + run = "curl -X POST $SLACK_WEBHOOK -d @payload.json" + +[pipeline.notifications] +on-success = { channel = "#deploys", message = "Deploy succeeded" } +on-failure = { channel = "#oncall", message = "Pipeline failed!" } diff --git a/tests/data/advanced/database-server.toml b/tests/data/advanced/database-server.toml new file mode 100644 index 0000000..efead75 --- /dev/null +++ b/tests/data/advanced/database-server.toml @@ -0,0 +1,164 @@ +# database-server.toml +# Database cluster configuration — TOML 1.0.0 deep nesting showcase +# Features: dotted top-level keys, quoted keys with dots/spaces, all four +# datetime forms, dotted table paths, array-of-tables with nested +# sub-tables and nested AoT, inline tables, all integer bases, +# all float forms, heterogeneous arrays. + +cluster.name = "primary-pg-cluster" +cluster.version = "16.2" +cluster.region = "eu-central-1" + +"cluster.id" = "pg-eur-001" +"127.0.0.1" = "loopback-alias" +'special chars' = "spaces in key name" + +# ── Temporal audit data ─────────────────────────────────────────────────────── + +[audit] +cluster-created = 1970-01-01T00:00:00Z +last-failover = 2024-03-10T02:30:00-05:00 +last-maintenance = 2024-01-28T22:00:00+01:00 +last-config-edit = 2024-07-22T11:45:30 +last-vacuum-start = 2024-07-21T03:00:00.000 +certificate-expiry = 2025-08-14 +created-date = 2022-09-01 +daily-backup-time = 03:30:00 +maintenance-window = 22:00:00 +checkpoint-time = 00:00:00.000 + +# ── Primary node ────────────────────────────────────────────────────────────── + +[nodes.primary] +host = "pg-primary.internal" +port = 5432 +max-connections = 500 +shared-buffers = "4GB" + +nodes.primary.replication.slots = 4 +nodes.primary.replication.timeout = 60 +nodes.primary.replication.mode = "async" + +[nodes.primary.wal] +level = "replica" +archive = true +archive-dir = "/mnt/wal-archive/primary" +compression = "lz4" +keep-segments = 16 + +[nodes.primary.checkpoint] +completion-target = 0.9 +warning-seconds = 30 + +# ── Replica nodes ───────────────────────────────────────────────────────────── + +[[nodes.replicas]] +name = "replica-01" +host = "pg-replica-01.internal" +port = 5432 +priority = 100 +lag-warning-ms = 5_000 +lag-critical-ms = 30_000 +sync-mode = "async" +promoted = false + + [nodes.replicas.connection] + pool-size = 20 + keepalive = { idle = 60, interval = 10, count = 5 } + + [[nodes.replicas.slots]] + name = "wal_slot_app_01" + plugin = "pgoutput" + active = true + + [[nodes.replicas.slots]] + name = "wal_slot_analytics" + plugin = "wal2json" + active = false + +[[nodes.replicas]] +name = "replica-02" +host = "pg-replica-02.internal" +port = 5432 +priority = 90 +lag-warning-ms = 5_000 +lag-critical-ms = 30_000 +sync-mode = "sync" +promoted = false + + [nodes.replicas.connection] + pool-size = 15 + keepalive = { idle = 60, interval = 10, count = 5 } + + [[nodes.replicas.slots]] + name = "wal_slot_app_02" + plugin = "pgoutput" + active = true + +# ── Storage — all numeric bases ─────────────────────────────────────────────── + +[storage] +block-size-decimal = 8192 +block-size-hex = 0x2000 +block-size-octal = 0o20000 +block-size-binary = 0b0010_0000_0000_0000 + +max-table-size-dec = 1_073_741_824 +max-table-size-hex = 0x4000_0000 + +file-permissions = 0o600 +cache-hit-target = 0.99 +bloat-threshold = 0.20 +fill-factor = 9.0e1 +compression-ratio = 3.5e+0 +dead-tuple-ratio = 1.0E-2 + +# ── Monitoring ──────────────────────────────────────────────────────────────── + +[monitoring] +scrape-interval = 15 +labels = { cluster = "primary-pg-cluster", env = "production" } + +alert-thresholds = [ + 0.8, + 0.9, + true, + "ops-team", +] + +retention-windows = [ + [60, "raw"], + [3600, "1h-rollup"], + [86400, "1d-rollup"], +] + +[[monitoring.alerts]] +name = "replication-lag" +severity = "critical" +threshold = 30_000 +enabled = true +created = 2023-11-01 + + [monitoring.alerts.notify] + channels = ["pagerduty", "slack-ops"] + cooldown = 300 + + [[monitoring.alerts.conditions]] + metric = "pg_replication_lag_bytes" + operator = ">" + value = 1_073_741_824 + +[[monitoring.alerts]] +name = "connection-saturation" +severity = "warning" +threshold = 0.85 +enabled = true + + [monitoring.alerts.notify] + channels = ["slack-ops"] + cooldown = 600 + + [[monitoring.alerts.conditions]] + metric = "pg_connections_ratio" + operator = ">=" + value = 0.85 diff --git a/tests/data/advanced/numbers-and-datetimes.toml b/tests/data/advanced/numbers-and-datetimes.toml new file mode 100644 index 0000000..ed5faff --- /dev/null +++ b/tests/data/advanced/numbers-and-datetimes.toml @@ -0,0 +1,103 @@ +# numbers-and-datetimes.toml +# Exhaustive showcase of every TOML 1.0.0 numeric and datetime form. +# Features: all integer literals, all float literals, all four datetime types, +# mixed/heterogeneous arrays, nested arrays, comments. + +# ── Integers ───────────────────────────────────────────────────────────────── + +[integers] +answer = 42 +positive = +42 +negative = -42 +zero = 0 +one-million = 1_000_000 +phone-num = 8_675_309 + +hex-upper = 0xDEADBEEF +hex-lower = 0xdeadbeef +hex-under = 0xdead_beef +octal-perms = 0o755 +octal-full = 0o01234567 +binary-byte = 0b1100_0011 +binary-long = 0b0001_0000_0000_0000_0000_0000 + +# ── Floats ─────────────────────────────────────────────────────────────────── + +[floats] +pi = 3.14159_26535 +neg-pi = -3.14 +pos-pi = +3.14 +zero-frac = 0.0 +neg-zero = -0.0 +small = 0.0123 + +avogadro = 6.022e23 +planck = 6.626e-34 +speed-c = 2.998e+8 +exp-upper = 1.0E6 +neg-e = -1e-1 + +under-int = 3_141.5927 +under-frac = 3141.592_7 +under-exp = 3e1_4 + +infinity = inf +neg-infinity = -inf +pos-infinity = +inf +not-a-number = nan +neg-nan = -nan +pos-nan = +nan + +# ── Datetimes ──────────────────────────────────────────────────────────────── + +[datetimes] +utc-zulu = 1987-07-05T17:45:56Z +utc-space-sep = 1987-07-05 17:45:00Z +utc-lowercase-z = 1987-07-05t17:45:00z +utc-millis = 2024-01-15T08:30:00.123Z + +eastern-us = 2024-06-21T09:00:00-04:00 +western-eu = 2024-06-21T15:00:00+02:00 +india = 2024-01-01T05:30:00+05:30 +nepal = 2024-01-01T05:45:00+05:45 + +local-dt = 1987-07-05T17:45:00 +local-dt-millis = 1977-12-21T10:32:00.555 +local-dt-space = 1987-07-05 17:45:00 + +birthday = 1987-07-05 +epoch-date = 1970-01-01 +leap-day = 2000-02-29 + +start-of-day = 00:00:00 +noon = 12:00:00 +end-of-day = 23:59:59 +with-millis = 17:45:00.123 + +# ── Mixed-type and nested arrays ───────────────────────────────────────────── + +[arrays] +integers = [1, 2, 3, 4, 5] +floats = [1.1, 2.2, 3.3] +strings = ["alpha", "beta", "gamma"] +booleans = [true, false, true] +mixed = [1, 1.5, "two", true] + +dates = [ + 1987-07-05T17:45:00Z, + 1979-05-27T07:32:00, + 2006-06-01, + 11:00:00, +] + +nested-strings = [["a", "b"], ["c", "d"], ["e"]] +nested-ints = [[1, 2], [3, 4, 5], [6]] +deeply-nested = [[[1, 2], [3]], [[4, 5]]] + +build-targets = [ + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "x86_64-apple-darwin", + "aarch64-apple-darwin", + "x86_64-pc-windows-msvc", +] From 2d14f86d89efcc71c404adb820a20417cec1e7cc Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 23 Jul 2026 19:37:01 +0200 Subject: [PATCH 03/13] docs: Add .LINK to all public function comment-based help Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/functions/public/Toml/ConvertFrom-Toml.ps1 | 3 +++ src/functions/public/Toml/ConvertTo-Toml.ps1 | 3 +++ src/functions/public/Toml/Export-Toml.ps1 | 3 +++ src/functions/public/Toml/Import-Toml.ps1 | 3 +++ 4 files changed, 12 insertions(+) diff --git a/src/functions/public/Toml/ConvertFrom-Toml.ps1 b/src/functions/public/Toml/ConvertFrom-Toml.ps1 index 5ab984a..097d7e7 100644 --- a/src/functions/public/Toml/ConvertFrom-Toml.ps1 +++ b/src/functions/public/Toml/ConvertFrom-Toml.ps1 @@ -6,6 +6,9 @@ function ConvertFrom-Toml { .DESCRIPTION Parses TOML-formatted text into a TomlDocument object with OrderedDictionary semantics and TOML-compatible scalar mappings. + + .LINK + https://psmodule.io/Toml/Functions/ConvertFrom-Toml #> [OutputType([TomlDocument])] [CmdletBinding()] diff --git a/src/functions/public/Toml/ConvertTo-Toml.ps1 b/src/functions/public/Toml/ConvertTo-Toml.ps1 index 348ca2d..886ac75 100644 --- a/src/functions/public/Toml/ConvertTo-Toml.ps1 +++ b/src/functions/public/Toml/ConvertTo-Toml.ps1 @@ -2,6 +2,9 @@ function ConvertTo-Toml { <# .SYNOPSIS Converts a PowerShell object graph to TOML text. + + .LINK + https://psmodule.io/Toml/Functions/ConvertTo-Toml #> [OutputType([string])] [CmdletBinding()] diff --git a/src/functions/public/Toml/Export-Toml.ps1 b/src/functions/public/Toml/Export-Toml.ps1 index b4ce236..4f338fd 100644 --- a/src/functions/public/Toml/Export-Toml.ps1 +++ b/src/functions/public/Toml/Export-Toml.ps1 @@ -36,6 +36,9 @@ - the object graph contains null values (TOML has no null) - the object graph contains types that cannot be serialized to TOML - the file cannot be created or written + + .LINK + https://psmodule.io/Toml/Functions/Export-Toml #> [CmdletBinding(SupportsShouldProcess)] param( diff --git a/src/functions/public/Toml/Import-Toml.ps1 b/src/functions/public/Toml/Import-Toml.ps1 index 51037e0..f92db74 100644 --- a/src/functions/public/Toml/Import-Toml.ps1 +++ b/src/functions/public/Toml/Import-Toml.ps1 @@ -33,6 +33,9 @@ - the file does not exist - the file cannot be read - the file content is not valid TOML 1.0.0 + + .LINK + https://psmodule.io/Toml/Functions/Import-Toml #> [OutputType([TomlDocument])] [CmdletBinding()] From 5ee6f97a55f31db484cced4fffd1294f825eab58 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 23 Jul 2026 19:39:30 +0200 Subject: [PATCH 04/13] refactor: Move functions to flat public/private layout, clean README - src/functions/public/Toml/ -> src/functions/public/ - src/functions/private/Toml/ -> src/functions/private/ - Remove template placeholder sections from README.md Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 31 +++++-------------- .../private/{Toml => }/Add-TomlTableText.ps1 | 0 .../ConvertFrom-TomlBasicStringValue.ps1 | 0 .../{Toml => }/ConvertFrom-TomlDateTime.ps1 | 0 .../ConvertFrom-TomlLiteralStringValue.ps1 | 0 .../ConvertFrom-TomlParsedValue.ps1 | 0 .../ConvertFrom-TomlScalarToken.ps1 | 0 .../{Toml => }/ConvertFrom-TomlTable.ps1 | 0 .../{Toml => }/ConvertFrom-TomlValue.ps1 | 0 .../{Toml => }/ConvertTo-TomlArrayObject.ps1 | 0 .../{Toml => }/ConvertTo-TomlTableObject.ps1 | 0 .../{Toml => }/ConvertTo-TomlValue.ps1 | 0 .../private/{Toml => }/Format-TomlKey.ps1 | 0 .../private/{Toml => }/Get-TomlBareToken.ps1 | 0 .../Get-TomlContentWithoutComment.ps1 | 0 .../{Toml => }/Get-TomlNestedTable.ps1 | 0 .../private/{Toml => }/Join-TomlKeyPath.ps1 | 0 .../{Toml => }/Skip-TomlWhitespace.ps1 | 0 .../{Toml => }/Split-TomlDottedKey.ps1 | 0 .../Test-TomlEndsWithDoubleNewLine.ps1 | 0 .../{Toml => }/Test-TomlTableArray.ps1 | 0 .../public/{Toml => }/ConvertFrom-Toml.ps1 | 0 .../public/{Toml => }/ConvertTo-Toml.ps1 | 0 .../public/{Toml => }/Export-Toml.ps1 | 0 .../public/{Toml => }/Import-Toml.ps1 | 0 25 files changed, 8 insertions(+), 23 deletions(-) rename src/functions/private/{Toml => }/Add-TomlTableText.ps1 (100%) rename src/functions/private/{Toml => }/ConvertFrom-TomlBasicStringValue.ps1 (100%) rename src/functions/private/{Toml => }/ConvertFrom-TomlDateTime.ps1 (100%) rename src/functions/private/{Toml => }/ConvertFrom-TomlLiteralStringValue.ps1 (100%) rename src/functions/private/{Toml => }/ConvertFrom-TomlParsedValue.ps1 (100%) rename src/functions/private/{Toml => }/ConvertFrom-TomlScalarToken.ps1 (100%) rename src/functions/private/{Toml => }/ConvertFrom-TomlTable.ps1 (100%) rename src/functions/private/{Toml => }/ConvertFrom-TomlValue.ps1 (100%) rename src/functions/private/{Toml => }/ConvertTo-TomlArrayObject.ps1 (100%) rename src/functions/private/{Toml => }/ConvertTo-TomlTableObject.ps1 (100%) rename src/functions/private/{Toml => }/ConvertTo-TomlValue.ps1 (100%) rename src/functions/private/{Toml => }/Format-TomlKey.ps1 (100%) rename src/functions/private/{Toml => }/Get-TomlBareToken.ps1 (100%) rename src/functions/private/{Toml => }/Get-TomlContentWithoutComment.ps1 (100%) rename src/functions/private/{Toml => }/Get-TomlNestedTable.ps1 (100%) rename src/functions/private/{Toml => }/Join-TomlKeyPath.ps1 (100%) rename src/functions/private/{Toml => }/Skip-TomlWhitespace.ps1 (100%) rename src/functions/private/{Toml => }/Split-TomlDottedKey.ps1 (100%) rename src/functions/private/{Toml => }/Test-TomlEndsWithDoubleNewLine.ps1 (100%) rename src/functions/private/{Toml => }/Test-TomlTableArray.ps1 (100%) rename src/functions/public/{Toml => }/ConvertFrom-Toml.ps1 (100%) rename src/functions/public/{Toml => }/ConvertTo-Toml.ps1 (100%) rename src/functions/public/{Toml => }/Export-Toml.ps1 (100%) rename src/functions/public/{Toml => }/Import-Toml.ps1 (100%) diff --git a/README.md b/README.md index 6e4b639..967fed8 100644 --- a/README.md +++ b/README.md @@ -105,30 +105,22 @@ Export-Toml -InputObject $doc -Path './config.toml' ## Implementation notes -- Implemented with in-repository PowerShell parser/serializer logic (no external TOML runtime dependency). +- Pure PowerShell parser and serializer — no external TOML runtime dependency. - Duplicate keys and table redefinition are rejected per the TOML 1.0.0 spec. - Files are written as UTF-8 without BOM. +## More examples -### Example 2 - -Provide examples for typical commands that a user would like to do with the module. +See the [examples](examples) folder for runnable scripts. Use PowerShell help for per-command examples: ```powershell -Import-Module -Name PSModuleTemplate +Get-Help ConvertFrom-Toml -Examples +Get-Help Import-Toml -Examples ``` -### Find more examples - -To find more examples of how to use the module, please refer to the [examples](examples) folder. - -Alternatively, you can use the Get-Command -Module 'This module' to find more commands that are available in the module. -To find examples of each of the commands you can use Get-Help -Examples 'CommandName'. - ## Documentation -Link to further documentation if available, or describe where in the repository users can find more detailed documentation about -the module's functions and features. +Full documentation is available at [psmodule.io/Toml](https://psmodule.io/Toml). ## Contributing @@ -136,15 +128,8 @@ Coder or not, you can contribute to the project! We welcome all contributions. ### For Users -If you don't code, you still sit on valuable information that can make this project even better. If you experience that the -product does unexpected things, throw errors or is missing functionality, you can help by submitting bugs and feature requests. -Please see the issues tab on this project and submit a new issue that matches your needs. +If you experience unexpected behavior, errors, or missing functionality, please open an issue on the [issues tab](https://github.com/PSModule/Toml/issues). ### For Developers -If you do code, we'd love to have your contributions. Please read the [Contribution guidelines](CONTRIBUTING.md) for more information. -You can either help by picking up an existing issue or submit a new one if you have an idea for a new feature or improvement. - -## Acknowledgements - -Here is a list of people and projects that helped this project in some way. +Please read the [Contribution guidelines](CONTRIBUTING.md) and pick up an existing issue or submit a new one. diff --git a/src/functions/private/Toml/Add-TomlTableText.ps1 b/src/functions/private/Add-TomlTableText.ps1 similarity index 100% rename from src/functions/private/Toml/Add-TomlTableText.ps1 rename to src/functions/private/Add-TomlTableText.ps1 diff --git a/src/functions/private/Toml/ConvertFrom-TomlBasicStringValue.ps1 b/src/functions/private/ConvertFrom-TomlBasicStringValue.ps1 similarity index 100% rename from src/functions/private/Toml/ConvertFrom-TomlBasicStringValue.ps1 rename to src/functions/private/ConvertFrom-TomlBasicStringValue.ps1 diff --git a/src/functions/private/Toml/ConvertFrom-TomlDateTime.ps1 b/src/functions/private/ConvertFrom-TomlDateTime.ps1 similarity index 100% rename from src/functions/private/Toml/ConvertFrom-TomlDateTime.ps1 rename to src/functions/private/ConvertFrom-TomlDateTime.ps1 diff --git a/src/functions/private/Toml/ConvertFrom-TomlLiteralStringValue.ps1 b/src/functions/private/ConvertFrom-TomlLiteralStringValue.ps1 similarity index 100% rename from src/functions/private/Toml/ConvertFrom-TomlLiteralStringValue.ps1 rename to src/functions/private/ConvertFrom-TomlLiteralStringValue.ps1 diff --git a/src/functions/private/Toml/ConvertFrom-TomlParsedValue.ps1 b/src/functions/private/ConvertFrom-TomlParsedValue.ps1 similarity index 100% rename from src/functions/private/Toml/ConvertFrom-TomlParsedValue.ps1 rename to src/functions/private/ConvertFrom-TomlParsedValue.ps1 diff --git a/src/functions/private/Toml/ConvertFrom-TomlScalarToken.ps1 b/src/functions/private/ConvertFrom-TomlScalarToken.ps1 similarity index 100% rename from src/functions/private/Toml/ConvertFrom-TomlScalarToken.ps1 rename to src/functions/private/ConvertFrom-TomlScalarToken.ps1 diff --git a/src/functions/private/Toml/ConvertFrom-TomlTable.ps1 b/src/functions/private/ConvertFrom-TomlTable.ps1 similarity index 100% rename from src/functions/private/Toml/ConvertFrom-TomlTable.ps1 rename to src/functions/private/ConvertFrom-TomlTable.ps1 diff --git a/src/functions/private/Toml/ConvertFrom-TomlValue.ps1 b/src/functions/private/ConvertFrom-TomlValue.ps1 similarity index 100% rename from src/functions/private/Toml/ConvertFrom-TomlValue.ps1 rename to src/functions/private/ConvertFrom-TomlValue.ps1 diff --git a/src/functions/private/Toml/ConvertTo-TomlArrayObject.ps1 b/src/functions/private/ConvertTo-TomlArrayObject.ps1 similarity index 100% rename from src/functions/private/Toml/ConvertTo-TomlArrayObject.ps1 rename to src/functions/private/ConvertTo-TomlArrayObject.ps1 diff --git a/src/functions/private/Toml/ConvertTo-TomlTableObject.ps1 b/src/functions/private/ConvertTo-TomlTableObject.ps1 similarity index 100% rename from src/functions/private/Toml/ConvertTo-TomlTableObject.ps1 rename to src/functions/private/ConvertTo-TomlTableObject.ps1 diff --git a/src/functions/private/Toml/ConvertTo-TomlValue.ps1 b/src/functions/private/ConvertTo-TomlValue.ps1 similarity index 100% rename from src/functions/private/Toml/ConvertTo-TomlValue.ps1 rename to src/functions/private/ConvertTo-TomlValue.ps1 diff --git a/src/functions/private/Toml/Format-TomlKey.ps1 b/src/functions/private/Format-TomlKey.ps1 similarity index 100% rename from src/functions/private/Toml/Format-TomlKey.ps1 rename to src/functions/private/Format-TomlKey.ps1 diff --git a/src/functions/private/Toml/Get-TomlBareToken.ps1 b/src/functions/private/Get-TomlBareToken.ps1 similarity index 100% rename from src/functions/private/Toml/Get-TomlBareToken.ps1 rename to src/functions/private/Get-TomlBareToken.ps1 diff --git a/src/functions/private/Toml/Get-TomlContentWithoutComment.ps1 b/src/functions/private/Get-TomlContentWithoutComment.ps1 similarity index 100% rename from src/functions/private/Toml/Get-TomlContentWithoutComment.ps1 rename to src/functions/private/Get-TomlContentWithoutComment.ps1 diff --git a/src/functions/private/Toml/Get-TomlNestedTable.ps1 b/src/functions/private/Get-TomlNestedTable.ps1 similarity index 100% rename from src/functions/private/Toml/Get-TomlNestedTable.ps1 rename to src/functions/private/Get-TomlNestedTable.ps1 diff --git a/src/functions/private/Toml/Join-TomlKeyPath.ps1 b/src/functions/private/Join-TomlKeyPath.ps1 similarity index 100% rename from src/functions/private/Toml/Join-TomlKeyPath.ps1 rename to src/functions/private/Join-TomlKeyPath.ps1 diff --git a/src/functions/private/Toml/Skip-TomlWhitespace.ps1 b/src/functions/private/Skip-TomlWhitespace.ps1 similarity index 100% rename from src/functions/private/Toml/Skip-TomlWhitespace.ps1 rename to src/functions/private/Skip-TomlWhitespace.ps1 diff --git a/src/functions/private/Toml/Split-TomlDottedKey.ps1 b/src/functions/private/Split-TomlDottedKey.ps1 similarity index 100% rename from src/functions/private/Toml/Split-TomlDottedKey.ps1 rename to src/functions/private/Split-TomlDottedKey.ps1 diff --git a/src/functions/private/Toml/Test-TomlEndsWithDoubleNewLine.ps1 b/src/functions/private/Test-TomlEndsWithDoubleNewLine.ps1 similarity index 100% rename from src/functions/private/Toml/Test-TomlEndsWithDoubleNewLine.ps1 rename to src/functions/private/Test-TomlEndsWithDoubleNewLine.ps1 diff --git a/src/functions/private/Toml/Test-TomlTableArray.ps1 b/src/functions/private/Test-TomlTableArray.ps1 similarity index 100% rename from src/functions/private/Toml/Test-TomlTableArray.ps1 rename to src/functions/private/Test-TomlTableArray.ps1 diff --git a/src/functions/public/Toml/ConvertFrom-Toml.ps1 b/src/functions/public/ConvertFrom-Toml.ps1 similarity index 100% rename from src/functions/public/Toml/ConvertFrom-Toml.ps1 rename to src/functions/public/ConvertFrom-Toml.ps1 diff --git a/src/functions/public/Toml/ConvertTo-Toml.ps1 b/src/functions/public/ConvertTo-Toml.ps1 similarity index 100% rename from src/functions/public/Toml/ConvertTo-Toml.ps1 rename to src/functions/public/ConvertTo-Toml.ps1 diff --git a/src/functions/public/Toml/Export-Toml.ps1 b/src/functions/public/Export-Toml.ps1 similarity index 100% rename from src/functions/public/Toml/Export-Toml.ps1 rename to src/functions/public/Export-Toml.ps1 diff --git a/src/functions/public/Toml/Import-Toml.ps1 b/src/functions/public/Import-Toml.ps1 similarity index 100% rename from src/functions/public/Toml/Import-Toml.ps1 rename to src/functions/public/Import-Toml.ps1 From f9d70daeb799deb919f620dd94789d8db9c00a99 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 23 Jul 2026 19:43:34 +0200 Subject: [PATCH 05/13] refactor: Remove obsolete src scaffolding files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove header.ps1, finally.ps1, init/initializer.ps1, manifest.psd1, and src/README.md — none are needed by the pure-PowerShell module. Build and 116 tests remain green. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/README.md | 3 --- src/finally.ps1 | 3 --- src/header.ps1 | 3 --- src/init/initializer.ps1 | 1 - src/manifest.psd1 | 6 ------ 5 files changed, 16 deletions(-) delete mode 100644 src/README.md delete mode 100644 src/finally.ps1 delete mode 100644 src/header.ps1 delete mode 100644 src/init/initializer.ps1 delete mode 100644 src/manifest.psd1 diff --git a/src/README.md b/src/README.md deleted file mode 100644 index af76160..0000000 --- a/src/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Details - -For more info about the expected structure of a module repository, please refer to [Build-PSModule](https://github.com/PSModule/Build-PSModule) diff --git a/src/finally.ps1 b/src/finally.ps1 deleted file mode 100644 index d8fc207..0000000 --- a/src/finally.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -Write-Verbose '------------------------------' -Write-Verbose '--- THIS IS A LAST LOADER ---' -Write-Verbose '------------------------------' diff --git a/src/header.ps1 b/src/header.ps1 deleted file mode 100644 index cc1fde9..0000000 --- a/src/header.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidLongLines', '', Justification = 'Contains long links.')] -[CmdletBinding()] -param() diff --git a/src/init/initializer.ps1 b/src/init/initializer.ps1 deleted file mode 100644 index 42703bd..0000000 --- a/src/init/initializer.ps1 +++ /dev/null @@ -1 +0,0 @@ -Write-Verbose 'Toml module initialized without external TOML assembly dependencies.' diff --git a/src/manifest.psd1 b/src/manifest.psd1 deleted file mode 100644 index 27ca5e1..0000000 --- a/src/manifest.psd1 +++ /dev/null @@ -1,6 +0,0 @@ -# This file always wins! -# Use this file to override any of the framework defaults and generated values. -@{ - ModuleVersion = '0.0.0' - PowerShellVersion = '7.6' -} From 19e20a569b87fe89784250e20152b9aff6097c8a Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 23 Jul 2026 19:58:20 +0200 Subject: [PATCH 06/13] test: Consolidate all tests into single Toml.Tests.ps1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - One file with Describe/Context hierarchy per command - Remove bootstrap.ps1 — module is pre-imported by the test runner - Add advanced fixture tests covering AoT sub-tables, space-separator datetimes, deep nesting, and all numeric forms - 120 tests, 0 failures Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/ConvertFrom-Toml.Tests.ps1 | 420 ---------------- tests/ConvertTo-Toml.Tests.ps1 | 212 -------- tests/Export-Toml.Tests.ps1 | 160 ------ tests/Import-Toml.Tests.ps1 | 114 ----- tests/Toml.Tests.ps1 | 812 ++++++++++++++++++++++++++++++- tests/bootstrap.ps1 | 17 - 6 files changed, 796 insertions(+), 939 deletions(-) delete mode 100644 tests/ConvertFrom-Toml.Tests.ps1 delete mode 100644 tests/ConvertTo-Toml.Tests.ps1 delete mode 100644 tests/Export-Toml.Tests.ps1 delete mode 100644 tests/Import-Toml.Tests.ps1 delete mode 100644 tests/bootstrap.ps1 diff --git a/tests/ConvertFrom-Toml.Tests.ps1 b/tests/ConvertFrom-Toml.Tests.ps1 deleted file mode 100644 index c563543..0000000 --- a/tests/ConvertFrom-Toml.Tests.ps1 +++ /dev/null @@ -1,420 +0,0 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester tests' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester tests' -)] -[CmdletBinding()] -param() - -Describe 'Toml' { - BeforeAll { - . (Join-Path $PSScriptRoot 'bootstrap.ps1') - } - - Describe 'ConvertFrom-Toml' { - - Context 'ConvertFrom-Toml - return type' { - It 'ConvertFrom-Toml - returns a TomlDocument for a minimal document' { - $result = ConvertFrom-Toml -InputObject 'key = "value"' - $result.GetType().Name | Should -Be 'TomlDocument' - } - - It 'ConvertFrom-Toml - Data property is an OrderedDictionary' { - $result = ConvertFrom-Toml -InputObject 'key = "value"' - $result.Data | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] - } - - It 'ConvertFrom-Toml - FilePath is null when parsed from string' { - $result = ConvertFrom-Toml -InputObject 'key = "value"' - $result.FilePath | Should -BeNullOrEmpty - } - - It 'ConvertFrom-Toml - accepts pipeline input' { - $result = 'key = "value"' | ConvertFrom-Toml - $result.GetType().Name | Should -Be 'TomlDocument' - } - } - - Context 'ConvertFrom-Toml - strings' { - It 'ConvertFrom-Toml - parses basic string' { - $result = ConvertFrom-Toml -InputObject 'key = "hello"' - $result.Data['key'] | Should -Be 'hello' - } - - It 'ConvertFrom-Toml - parses literal string' { - $result = ConvertFrom-Toml -InputObject "key = 'C:\path'" - $result.Data['key'] | Should -Be 'C:\path' - } - - It 'ConvertFrom-Toml - parses basic string with escape sequences' { - $result = ConvertFrom-Toml -InputObject 'key = "tab\there"' - $result.Data['key'] | Should -Be "tab`there" - } - - It 'ConvertFrom-Toml - parses basic string with quoted escape' { - $result = ConvertFrom-Toml -InputObject 'key = "say \"hi\""' - $result.Data['key'] | Should -Be 'say "hi"' - } - - It 'ConvertFrom-Toml - parses Unicode escape sequence' { - $result = ConvertFrom-Toml -InputObject 'key = "\u03B1"' - $result.Data['key'] | Should -Be ([char]0x03B1).ToString() - } - - It 'ConvertFrom-Toml - parses multi-line basic string' { - $toml = "key = `"`"`"`nline one`nline two`n`"`"`"" - $result = ConvertFrom-Toml -InputObject $toml - $result.Data['key'] | Should -Match 'line one' - $result.Data['key'] | Should -Match 'line two' - } - - It 'ConvertFrom-Toml - parses multi-line literal string' { - $toml = "key = '''" + [System.Environment]::NewLine + "raw\nno escape" + [System.Environment]::NewLine + "'''" - $result = ConvertFrom-Toml -InputObject $toml - $result.Data['key'] | Should -Match 'raw\\nno escape' - } - - It 'ConvertFrom-Toml - parses empty string' { - $result = ConvertFrom-Toml -InputObject 'key = ""' - $result.Data['key'] | Should -Be '' - } - - It 'ConvertFrom-Toml - parses strings from file' { - $path = Join-Path $PSScriptRoot 'data\strings.toml' - $result = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) - $result.Data['literal'] | Should -Be 'C:\Users\nodejs\templates' - $result.Data['empty'] | Should -Be '' - } - } - - Context 'ConvertFrom-Toml - integers' { - It 'ConvertFrom-Toml - parses decimal integer' { - $result = ConvertFrom-Toml -InputObject 'n = 42' - $result.Data['n'] | Should -Be 42 - $result.Data['n'] | Should -BeOfType [long] - } - - It 'ConvertFrom-Toml - parses negative integer' { - $result = ConvertFrom-Toml -InputObject 'n = -17' - $result.Data['n'] | Should -Be -17 - } - - It 'ConvertFrom-Toml - parses zero' { - $result = ConvertFrom-Toml -InputObject 'n = 0' - $result.Data['n'] | Should -Be 0 - } - - It 'ConvertFrom-Toml - parses integer with underscore separator' { - $result = ConvertFrom-Toml -InputObject 'n = 1_000_000' - $result.Data['n'] | Should -Be 1000000 - } - - It 'ConvertFrom-Toml - parses hexadecimal integer' { - $result = ConvertFrom-Toml -InputObject 'n = 0xFF' - $result.Data['n'] | Should -Be 255 - } - - It 'ConvertFrom-Toml - parses octal integer' { - $result = ConvertFrom-Toml -InputObject 'n = 0o17' - $result.Data['n'] | Should -Be 15 - } - - It 'ConvertFrom-Toml - parses binary integer' { - $result = ConvertFrom-Toml -InputObject 'n = 0b1010' - $result.Data['n'] | Should -Be 10 - } - - It 'ConvertFrom-Toml - parses all integer forms from file' { - $path = Join-Path $PSScriptRoot 'data\integers.toml' - $result = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) - $result.Data['decimal'] | Should -Be 42 - $result.Data['negative'] | Should -Be -17 - $result.Data['hex'] | Should -Be 3735928559 # 0xDEADBEEF as unsigned long - $result.Data['octal'] | Should -Be 493 # 0o755 - $result.Data['binary'] | Should -Be 214 # 0b11010110 - } - } - - Context 'ConvertFrom-Toml - floats' { - It 'ConvertFrom-Toml - parses positive float' { - $result = ConvertFrom-Toml -InputObject 'n = 3.14' - $result.Data['n'] | Should -Be 3.14 - $result.Data['n'] | Should -BeOfType [double] - } - - It 'ConvertFrom-Toml - parses negative float' { - $result = ConvertFrom-Toml -InputObject 'n = -0.01' - $result.Data['n'] | Should -Be -0.01 - } - - It 'ConvertFrom-Toml - parses scientific notation' { - $result = ConvertFrom-Toml -InputObject 'n = 5e+22' - $result.Data['n'] | Should -Be 5e22 - } - - It 'ConvertFrom-Toml - parses inf' { - $result = ConvertFrom-Toml -InputObject 'n = inf' - $result.Data['n'] | Should -Be ([double]::PositiveInfinity) - } - - It 'ConvertFrom-Toml - parses -inf' { - $result = ConvertFrom-Toml -InputObject 'n = -inf' - $result.Data['n'] | Should -Be ([double]::NegativeInfinity) - } - - It 'ConvertFrom-Toml - parses nan' { - $result = ConvertFrom-Toml -InputObject 'n = nan' - [double]::IsNaN($result.Data['n']) | Should -BeTrue - } - - It 'ConvertFrom-Toml - parses floats from file' { - $path = Join-Path $PSScriptRoot 'data\floats.toml' - $result = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) - ([double]$result.Data['positive'] - 3.1415) | Should -BeLessThan 0.0001 - ([double]$result.Data['negative'] - (-0.01)) | Should -BeLessThan 0.0001 - } - } - - Context 'ConvertFrom-Toml - booleans' { - It 'ConvertFrom-Toml - parses true' { - $result = ConvertFrom-Toml -InputObject 'flag = true' - $result.Data['flag'] | Should -Be $true - $result.Data['flag'] | Should -BeOfType [bool] - } - - It 'ConvertFrom-Toml - parses false' { - $result = ConvertFrom-Toml -InputObject 'flag = false' - $result.Data['flag'] | Should -Be $false - } - } - - Context 'ConvertFrom-Toml - datetimes' { - It 'ConvertFrom-Toml - parses offset datetime with Z suffix as DateTimeOffset' { - $result = ConvertFrom-Toml -InputObject 'dt = 1979-05-27T07:32:00Z' - $result.Data['dt'] | Should -BeOfType [System.DateTimeOffset] - ([System.DateTimeOffset]$result.Data['dt']).Year | Should -Be 1979 - ([System.DateTimeOffset]$result.Data['dt']).Month | Should -Be 5 - ([System.DateTimeOffset]$result.Data['dt']).Day | Should -Be 27 - } - - It 'ConvertFrom-Toml - parses offset datetime with numeric offset as DateTimeOffset' { - $result = ConvertFrom-Toml -InputObject 'dt = 1979-05-27T07:32:00+05:30' - $result.Data['dt'] | Should -BeOfType [System.DateTimeOffset] - $offset = ([System.DateTimeOffset]$result.Data['dt']).Offset - $offset.Hours | Should -Be 5 - $offset.Minutes | Should -Be 30 - } - - It 'ConvertFrom-Toml - parses local datetime as DateTime with Unspecified kind' { - $result = ConvertFrom-Toml -InputObject 'dt = 1979-05-27T07:32:00' - $result.Data['dt'] | Should -BeOfType [System.DateTime] - ([System.DateTime]$result.Data['dt']).Kind | Should -Be ([System.DateTimeKind]::Unspecified) - ([System.DateTime]$result.Data['dt']).Year | Should -Be 1979 - } - - It 'ConvertFrom-Toml - parses local date as DateTime with zero time' { - $result = ConvertFrom-Toml -InputObject 'dt = 1979-05-27' - $result.Data['dt'] | Should -BeOfType [System.DateTime] - $dt = [System.DateTime]$result.Data['dt'] - $dt.Year | Should -Be 1979 - $dt.Month | Should -Be 5 - $dt.Day | Should -Be 27 - $dt.Hour | Should -Be 0 - $dt.Minute | Should -Be 0 - $dt.Second | Should -Be 0 - } - - It 'ConvertFrom-Toml - parses local time as TimeSpan' { - $result = ConvertFrom-Toml -InputObject 'tm = 07:32:00' - $result.Data['tm'] | Should -BeOfType [System.TimeSpan] - ([System.TimeSpan]$result.Data['tm']).Hours | Should -Be 7 - ([System.TimeSpan]$result.Data['tm']).Minutes | Should -Be 32 - } - - It 'ConvertFrom-Toml - parses all datetime types from file' { - $path = Join-Path $PSScriptRoot 'data\datetimes.toml' - $result = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) - $result.Data['offset_dt_z'] | Should -BeOfType [System.DateTimeOffset] - $result.Data['offset_dt_num'] | Should -BeOfType [System.DateTimeOffset] - $result.Data['local_dt'] | Should -BeOfType [System.DateTime] - $result.Data['local_date'] | Should -BeOfType [System.DateTime] - $result.Data['local_time'] | Should -BeOfType [System.TimeSpan] - } - } - - Context 'ConvertFrom-Toml - arrays' { - It 'ConvertFrom-Toml - parses integer array' { - $result = ConvertFrom-Toml -InputObject 'arr = [1, 2, 3]' - $result.Data['arr'] | Should -HaveCount 3 - $result.Data['arr'][0] | Should -Be 1 - $result.Data['arr'][2] | Should -Be 3 - } - - It 'ConvertFrom-Toml - parses string array' { - $result = ConvertFrom-Toml -InputObject 'arr = ["a", "b", "c"]' - $result.Data['arr'][0] | Should -Be 'a' - $result.Data['arr'][1] | Should -Be 'b' - } - - It 'ConvertFrom-Toml - parses empty array' { - $result = ConvertFrom-Toml -InputObject 'arr = []' - $result.Data['arr'] | Should -HaveCount 0 - } - - It 'ConvertFrom-Toml - parses nested array' { - $result = ConvertFrom-Toml -InputObject 'arr = [[1, 2], [3, 4]]' - $result.Data['arr'] | Should -HaveCount 2 - $result.Data['arr'][0] | Should -HaveCount 2 - $result.Data['arr'][0][1] | Should -Be 2 - } - - It 'ConvertFrom-Toml - parses multiline array' { - $toml = "arr = [`n 1,`n 2,`n 3,`n]" - $result = ConvertFrom-Toml -InputObject $toml - $result.Data['arr'] | Should -HaveCount 3 - } - - It 'ConvertFrom-Toml - parses arrays from file' { - $path = Join-Path $PSScriptRoot 'data\arrays.toml' - $result = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) - $result.Data['integers'] | Should -HaveCount 3 - $result.Data['strings'] | Should -HaveCount 3 - $result.Data['empty'] | Should -HaveCount 0 - $result.Data['nested'] | Should -HaveCount 2 - } - } - - Context 'ConvertFrom-Toml - tables' { - It 'ConvertFrom-Toml - parses standard table' { - $toml = "[server]`nhost = `"localhost`"" - $result = ConvertFrom-Toml -InputObject $toml - $result.Data['server'] | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] - $result.Data['server']['host'] | Should -Be 'localhost' - } - - It 'ConvertFrom-Toml - parses inline table' { - $result = ConvertFrom-Toml -InputObject 'point = { x = 1, y = 2 }' - $result.Data['point'] | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] - $result.Data['point']['x'] | Should -Be 1 - $result.Data['point']['y'] | Should -Be 2 - } - - It 'ConvertFrom-Toml - parses nested tables via dotted header' { - $toml = "[a.b.c]`nkey = `"deep`"" - $result = ConvertFrom-Toml -InputObject $toml - $result.Data['a']['b']['c']['key'] | Should -Be 'deep' - } - - It 'ConvertFrom-Toml - tables from file' { - $path = Join-Path $PSScriptRoot 'data\tables.toml' - $result = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) - $result.Data['simple']['key'] | Should -Be 'value' - $result.Data['dotted']['key']['works'] | Should -Be $true - $result.Data['inline_parent']['inline']['one'] | Should -Be 1 - } - } - - Context 'ConvertFrom-Toml - array of tables' { - It 'ConvertFrom-Toml - parses array of tables' { - $path = Join-Path $PSScriptRoot 'data\array-of-tables.toml' - $result = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) - $result.Data['products'] | Should -HaveCount 2 - $result.Data['products'][0]['name'] | Should -Be 'Hammer' - $result.Data['products'][1]['name'] | Should -Be 'Nail' - } - - It 'ConvertFrom-Toml - parses nested array of tables' { - $path = Join-Path $PSScriptRoot 'data\array-of-tables.toml' - $result = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) - $result.Data['fruits'][0]['varieties'] | Should -HaveCount 2 - $result.Data['fruits'][0]['varieties'][0]['name'] | Should -Be 'red delicious' - } - } - - Context 'ConvertFrom-Toml - keys' { - It 'ConvertFrom-Toml - parses bare key' { - $result = ConvertFrom-Toml -InputObject 'bare_key = "value"' - $result.Data['bare_key'] | Should -Be 'value' - } - - It 'ConvertFrom-Toml - parses quoted key' { - $result = ConvertFrom-Toml -InputObject '"quoted key" = "value"' - $result.Data['quoted key'] | Should -Be 'value' - } - - It 'ConvertFrom-Toml - parses dotted key' { - $result = ConvertFrom-Toml -InputObject 'a.b = "value"' - $result.Data['a']['b'] | Should -Be 'value' - } - - It 'ConvertFrom-Toml - preserves key order' { - $toml = "z = 1`ny = 2`nx = 3" - $result = ConvertFrom-Toml -InputObject $toml - $keys = $result.Data.Keys - $keys[0] | Should -Be 'z' - $keys[1] | Should -Be 'y' - $keys[2] | Should -Be 'x' - } - } - - Context 'ConvertFrom-Toml - comments and whitespace' { - It 'ConvertFrom-Toml - ignores inline comments' { - $result = ConvertFrom-Toml -InputObject 'key = "value" # this is a comment' - $result.Data['key'] | Should -Be 'value' - } - - It 'ConvertFrom-Toml - ignores full-line comments' { - $toml = "# comment`nkey = `"value`"" - $result = ConvertFrom-Toml -InputObject $toml - $result.Data['key'] | Should -Be 'value' - } - - It 'ConvertFrom-Toml - handles CRLF line endings' { - $toml = "a = 1`r`nb = 2" - $result = ConvertFrom-Toml -InputObject $toml - $result.Data['a'] | Should -Be 1 - $result.Data['b'] | Should -Be 2 - } - } - - Context 'ConvertFrom-Toml - full example file' { - It 'ConvertFrom-Toml - parses full example document' { - $path = Join-Path $PSScriptRoot 'data\full-example.toml' - $result = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) - $result.Data['title'] | Should -Be 'TOML Example' - $result.Data['owner']['name'] | Should -Be 'Tom Preston-Werner' - $result.Data['database']['enabled'] | Should -Be $true - $result.Data['database']['ports'] | Should -HaveCount 3 - $result.Data['servers']['alpha']['ip'] | Should -Be '10.0.0.1' - $result.Data['servers']['beta']['role'] | Should -Be 'backend' - } - } - - Context 'ConvertFrom-Toml - error handling' { - It 'ConvertFrom-Toml - throws on invalid TOML syntax' { - { ConvertFrom-Toml -InputObject 'invalid = = "bad"' } | Should -Throw - } - - It 'ConvertFrom-Toml - throws on duplicate key' { - $toml = "key = 1`nkey = 2" - { ConvertFrom-Toml -InputObject $toml } | Should -Throw - } - - It 'ConvertFrom-Toml - throws on missing value' { - { ConvertFrom-Toml -InputObject 'key =' } | Should -Throw - } - - It 'ConvertFrom-Toml - throws on empty string input' { - { ConvertFrom-Toml -InputObject '' } | Should -Throw - } - - It 'ConvertFrom-Toml - throws on redefining a table as a key' { - $toml = "[a]`nkey = 1`n[a]`nother = 2" - { ConvertFrom-Toml -InputObject $toml } | Should -Throw - } - } - } -} diff --git a/tests/ConvertTo-Toml.Tests.ps1 b/tests/ConvertTo-Toml.Tests.ps1 deleted file mode 100644 index 04d455b..0000000 --- a/tests/ConvertTo-Toml.Tests.ps1 +++ /dev/null @@ -1,212 +0,0 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester tests' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester tests' -)] -[CmdletBinding()] -param() - -Describe 'Toml' { - BeforeAll { - . (Join-Path $PSScriptRoot 'bootstrap.ps1') - } - - Describe 'ConvertTo-Toml' { - - Context 'ConvertTo-Toml - return type' { - It 'ConvertTo-Toml - returns a string' { - $result = ConvertTo-Toml -InputObject ([ordered]@{ key = 'value' }) - $result | Should -BeOfType [string] - } - - It 'ConvertTo-Toml - result is non-empty for non-empty input' { - $result = ConvertTo-Toml -InputObject ([ordered]@{ key = 'value' }) - $result | Should -Not -BeNullOrEmpty - } - - It 'ConvertTo-Toml - accepts pipeline input' { - $result = [ordered]@{ key = 'value' } | ConvertTo-Toml - $result | Should -BeOfType [string] - } - } - - Context 'ConvertTo-Toml - string serialization' { - It 'ConvertTo-Toml - serializes a simple string key' { - $result = ConvertTo-Toml -InputObject ([ordered]@{ title = 'Hello' }) - $result | Should -Match 'title = "Hello"' - } - - It 'ConvertTo-Toml - output is valid TOML that round-trips' { - $original = [ordered]@{ key = 'value' } - $toml = ConvertTo-Toml -InputObject $original - $roundTrip = ConvertFrom-Toml -InputObject $toml - $roundTrip.Data['key'] | Should -Be 'value' - } - } - - Context 'ConvertTo-Toml - integer serialization' { - It 'ConvertTo-Toml - serializes integer' { - $result = ConvertTo-Toml -InputObject ([ordered]@{ n = [long]42 }) - $result | Should -Match 'n = 42' - } - - It 'ConvertTo-Toml - integer round-trips correctly' { - $original = [ordered]@{ n = [long]12345 } - $toml = ConvertTo-Toml -InputObject $original - $rt = ConvertFrom-Toml -InputObject $toml - $rt.Data['n'] | Should -Be 12345 - } - } - - Context 'ConvertTo-Toml - float serialization' { - It 'ConvertTo-Toml - serializes float' { - $result = ConvertTo-Toml -InputObject ([ordered]@{ pi = 3.14 }) - $result | Should -Match 'pi = ' - $result | Should -Match '3.14' - } - - It 'ConvertTo-Toml - float round-trips correctly' { - $original = [ordered]@{ n = 2.718 } - $toml = ConvertTo-Toml -InputObject $original - $rt = ConvertFrom-Toml -InputObject $toml - ([double]$rt.Data['n'] - 2.718) | Should -BeLessThan 0.001 - } - } - - Context 'ConvertTo-Toml - boolean serialization' { - It 'ConvertTo-Toml - serializes true' { - $result = ConvertTo-Toml -InputObject ([ordered]@{ flag = $true }) - $result | Should -Match 'flag = true' - } - - It 'ConvertTo-Toml - serializes false' { - $result = ConvertTo-Toml -InputObject ([ordered]@{ flag = $false }) - $result | Should -Match 'flag = false' - } - } - - Context 'ConvertTo-Toml - datetime serialization' { - It 'ConvertTo-Toml - serializes DateTimeOffset as offset datetime' { - $dto = [System.DateTimeOffset]::new(1979, 5, 27, 7, 32, 0, [System.TimeSpan]::Zero) - $result = ConvertTo-Toml -InputObject ([ordered]@{ dt = $dto }) - $result | Should -Match '1979-05-27' - } - - It 'ConvertTo-Toml - DateTimeOffset round-trips' { - $dto = [System.DateTimeOffset]::new(2024, 3, 15, 12, 0, 0, [System.TimeSpan]::FromHours(2)) - $original = [ordered]@{ dt = $dto } - $toml = ConvertTo-Toml -InputObject $original - $rt = ConvertFrom-Toml -InputObject $toml - $rt.Data['dt'] | Should -BeOfType [System.DateTimeOffset] - ([System.DateTimeOffset]$rt.Data['dt']).Year | Should -Be 2024 - } - - It 'ConvertTo-Toml - serializes DateTime with time as local datetime' { - $dt = [System.DateTime]::new(2024, 6, 1, 15, 30, 0, [System.DateTimeKind]::Unspecified) - $result = ConvertTo-Toml -InputObject ([ordered]@{ dt = $dt }) - $result | Should -Match '2024-06-01' - } - - It 'ConvertTo-Toml - serializes date-only DateTime as local date' { - $dt = [System.DateTime]::new(2024, 6, 1, 0, 0, 0, [System.DateTimeKind]::Unspecified) - $result = ConvertTo-Toml -InputObject ([ordered]@{ dt = $dt }) - $result | Should -Match '2024-06-01' - } - - It 'ConvertTo-Toml - serializes TimeSpan as local time' { - $ts = [System.TimeSpan]::new(0, 8, 30, 0) - $result = ConvertTo-Toml -InputObject ([ordered]@{ tm = $ts }) - $result | Should -Match '08:30:00' - } - } - - Context 'ConvertTo-Toml - nested table serialization' { - It 'ConvertTo-Toml - serializes nested ordered dict as TOML table' { - $obj = [ordered]@{ - server = [ordered]@{ host = 'localhost'; port = [long]8080 } - } - $result = ConvertTo-Toml -InputObject $obj - $result | Should -Match '\[server\]' - $result | Should -Match 'host = "localhost"' - } - - It 'ConvertTo-Toml - nested table round-trips' { - $original = [ordered]@{ - database = [ordered]@{ host = 'db.example.com'; port = [long]5432 } - } - $toml = ConvertTo-Toml -InputObject $original - $rt = ConvertFrom-Toml -InputObject $toml - $rt.Data['database']['host'] | Should -Be 'db.example.com' - $rt.Data['database']['port'] | Should -Be 5432 - } - } - - Context 'ConvertTo-Toml - array serialization' { - It 'ConvertTo-Toml - serializes array of integers' { - $obj = [ordered]@{ ports = @([long]80, [long]443, [long]8080) } - $result = ConvertTo-Toml -InputObject $obj - $result | Should -Match '80' - $result | Should -Match '443' - } - - It 'ConvertTo-Toml - integer array round-trips' { - $original = [ordered]@{ nums = @([long]1, [long]2, [long]3) } - $toml = ConvertTo-Toml -InputObject $original - $rt = ConvertFrom-Toml -InputObject $toml - $rt.Data['nums'] | Should -HaveCount 3 - $rt.Data['nums'][0] | Should -Be 1 - } - } - - Context 'ConvertTo-Toml - PSCustomObject input' { - It 'ConvertTo-Toml - serializes PSCustomObject' { - $obj = [PSCustomObject]@{ name = 'Alice'; age = [long]30 } - $result = ConvertTo-Toml -InputObject $obj - $result | Should -Match 'name = "Alice"' - $result | Should -Match 'age = 30' - } - } - - Context 'ConvertTo-Toml - TomlDocument input' { - It 'ConvertTo-Toml - accepts TomlDocument from ConvertFrom-Toml' { - $toml = "title = `"Test`"`n[section]`nvalue = 42" - $doc = ConvertFrom-Toml -InputObject $toml - $result = ConvertTo-Toml -InputObject $doc - $result | Should -Match 'title = "Test"' - $result | Should -Match '\[section\]' - } - } - - Context 'ConvertTo-Toml - round-trip' { - It 'ConvertTo-Toml - full example round-trips losslessly for string/integer/boolean/float' { - $path = Join-Path $PSScriptRoot 'data\full-example.toml' - $original = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) - $toml = ConvertTo-Toml -InputObject $original - $rt = ConvertFrom-Toml -InputObject $toml - $rt.Data['title'] | Should -Be 'TOML Example' - $rt.Data['owner']['name'] | Should -Be 'Tom Preston-Werner' - $rt.Data['database']['enabled'] | Should -Be $true - $rt.Data['database']['ports'] | Should -HaveCount 3 - $rt.Data['servers']['alpha']['ip'] | Should -Be '10.0.0.1' - } - - It 'ConvertTo-Toml - array of integers round-trips' { - $path = Join-Path $PSScriptRoot 'data\arrays.toml' - $original = ConvertFrom-Toml -InputObject (Get-Content $path -Raw) - $toml = ConvertTo-Toml -InputObject $original - $rt = ConvertFrom-Toml -InputObject $toml - $rt.Data['integers'] | Should -HaveCount 3 - $rt.Data['strings'] | Should -HaveCount 3 - } - } - - Context 'ConvertTo-Toml - error handling' { - It 'ConvertTo-Toml - throws on null input' { - { ConvertTo-Toml -InputObject $null } | Should -Throw - } - } - } -} diff --git a/tests/Export-Toml.Tests.ps1 b/tests/Export-Toml.Tests.ps1 deleted file mode 100644 index 9cd06aa..0000000 --- a/tests/Export-Toml.Tests.ps1 +++ /dev/null @@ -1,160 +0,0 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester tests' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester tests' -)] -[CmdletBinding()] -param() - -Describe 'Toml' { - BeforeAll { - . (Join-Path $PSScriptRoot 'bootstrap.ps1') - } - - Describe 'Export-Toml' { - - Context 'Export-Toml - basic write' { - It 'Export-Toml - creates a file at the given path' { - $path = Join-Path $TestDrive 'output.toml' - Export-Toml -InputObject ([ordered]@{ key = 'value' }) -Path $path - Test-Path -Path $path | Should -BeTrue - } - - It 'Export-Toml - file content is valid TOML' { - $path = Join-Path $TestDrive 'output.toml' - Export-Toml -InputObject ([ordered]@{ title = 'Hello' }) -Path $path - $content = Get-Content -Path $path -Raw - $content | Should -Match 'title = "Hello"' - } - - It 'Export-Toml - returns nothing to the output stream' { - $path = Join-Path $TestDrive 'void.toml' - $result = Export-Toml -InputObject ([ordered]@{ k = 'v' }) -Path $path - $result | Should -BeNullOrEmpty - } - - It 'Export-Toml - creates parent directory when it does not exist' { - $path = Join-Path $TestDrive 'nested\subdir\output.toml' - Export-Toml -InputObject ([ordered]@{ k = 'v' }) -Path $path - Test-Path -Path $path | Should -BeTrue - } - } - - Context 'Export-Toml - round-trip via Import-Toml' { - It 'Export-Toml - round-trip preserves string values' { - $inputDoc = [ordered]@{ greeting = 'Hello World' } - $path = Join-Path $TestDrive 'rt-string.toml' - Export-Toml -InputObject $inputDoc -Path $path - $result = Import-Toml -Path $path - $result.Data['greeting'] | Should -Be 'Hello World' - } - - It 'Export-Toml - round-trip preserves integer values' { - $inputDoc = [ordered]@{ count = [long]42 } - $path = Join-Path $TestDrive 'rt-int.toml' - Export-Toml -InputObject $inputDoc -Path $path - $result = Import-Toml -Path $path - $result.Data['count'] | Should -Be 42 - } - - It 'Export-Toml - round-trip preserves boolean values' { - $inputDoc = [ordered]@{ enabled = $true; disabled = $false } - $path = Join-Path $TestDrive 'rt-bool.toml' - Export-Toml -InputObject $inputDoc -Path $path - $result = Import-Toml -Path $path - $result.Data['enabled'] | Should -Be $true - $result.Data['disabled'] | Should -Be $false - } - - It 'Export-Toml - round-trip preserves float values' { - $inputDoc = [ordered]@{ pi = 3.14159 } - $path = Join-Path $TestDrive 'rt-float.toml' - Export-Toml -InputObject $inputDoc -Path $path - $result = Import-Toml -Path $path - ([double]$result.Data['pi'] - 3.14159) | Should -BeLessThan 0.00001 - } - - It 'Export-Toml - round-trip preserves nested tables' { - $inputDoc = [ordered]@{ - server = [ordered]@{ host = 'localhost'; port = [long]8080 } - } - $path = Join-Path $TestDrive 'rt-nested.toml' - Export-Toml -InputObject $inputDoc -Path $path - $result = Import-Toml -Path $path - $result.Data['server']['host'] | Should -Be 'localhost' - $result.Data['server']['port'] | Should -Be 8080 - } - - It 'Export-Toml - round-trip preserves integer arrays' { - $inputDoc = [ordered]@{ ports = @([long]80, [long]443) } - $path = Join-Path $TestDrive 'rt-array.toml' - Export-Toml -InputObject $inputDoc -Path $path - $result = Import-Toml -Path $path - $result.Data['ports'] | Should -HaveCount 2 - $result.Data['ports'][0] | Should -Be 80 - } - - It 'Export-Toml - full file round-trip' { - $srcPath = Join-Path $PSScriptRoot 'data\full-example.toml' - $original = Import-Toml -Path $srcPath - - $outPath = Join-Path $TestDrive 'rt-full.toml' - Export-Toml -InputObject $original -Path $outPath - - $rt = Import-Toml -Path $outPath - $rt.Data['title'] | Should -Be 'TOML Example' - $rt.Data['owner']['name'] | Should -Be 'Tom Preston-Werner' - $rt.Data['database']['enabled'] | Should -Be $true - $rt.Data['servers']['alpha']['ip'] | Should -Be '10.0.0.1' - } - } - - Context 'Export-Toml - UTF-8 encoding' { - It 'Export-Toml - writes UTF-8 without BOM' { - $path = Join-Path $TestDrive 'utf8.toml' - Export-Toml -InputObject ([ordered]@{ key = 'αβγ' }) -Path $path - $bytes = [System.IO.File]::ReadAllBytes($path) - # UTF-8 BOM is EF BB BF — should not be present - if ($bytes.Length -ge 3) { - ($bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) | Should -BeFalse - } - } - - It 'Export-Toml - round-trips Unicode string content correctly' { - $path = Join-Path $TestDrive 'unicode.toml' - Export-Toml -InputObject ([ordered]@{ msg = 'こんにちは' }) -Path $path - $result = Import-Toml -Path $path - $result.Data['msg'] | Should -Be 'こんにちは' - } - } - - Context 'Export-Toml - pipeline input' { - It 'Export-Toml - accepts TomlDocument from pipeline' { - $srcPath = Join-Path $PSScriptRoot 'data\booleans.toml' - $outPath = Join-Path $TestDrive 'from-pipeline.toml' - Import-Toml -Path $srcPath | Export-Toml -Path $outPath - Test-Path $outPath | Should -BeTrue - $rt = Import-Toml -Path $outPath - $rt.Data['enabled'] | Should -Be $true - } - } - - Context 'Export-Toml - WhatIf support' { - It 'Export-Toml - WhatIf does not create file' { - $path = Join-Path $TestDrive 'whatif.toml' - Export-Toml -InputObject ([ordered]@{ k = 'v' }) -Path $path -WhatIf - Test-Path -Path $path | Should -BeFalse - } - } - - Context 'Export-Toml - error handling' { - It 'Export-Toml - throws on null InputObject' { - $path = Join-Path $TestDrive 'null.toml' - { Export-Toml -InputObject $null -Path $path } | Should -Throw - } - } - } -} diff --git a/tests/Import-Toml.Tests.ps1 b/tests/Import-Toml.Tests.ps1 deleted file mode 100644 index 8668d83..0000000 --- a/tests/Import-Toml.Tests.ps1 +++ /dev/null @@ -1,114 +0,0 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester tests' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester tests' -)] -[CmdletBinding()] -param() - -Describe 'Toml' { - Describe 'Import-Toml' { - BeforeAll { - . (Join-Path $PSScriptRoot 'bootstrap.ps1') - $testDataDir = Join-Path $PSScriptRoot 'data' - } - - Context 'Import-Toml - return type' { - It 'Import-Toml - returns a TomlDocument' { - $path = Join-Path $testDataDir 'full-example.toml' - $result = Import-Toml -Path $path - $result.GetType().Name | Should -Be 'TomlDocument' - } - - It 'Import-Toml - sets FilePath to resolved absolute path' { - $path = Join-Path $testDataDir 'full-example.toml' - $result = Import-Toml -Path $path - $result.FilePath | Should -Not -BeNullOrEmpty - [System.IO.Path]::IsPathRooted($result.FilePath) | Should -BeTrue - } - - It 'Import-Toml - Data property is an OrderedDictionary' { - $path = Join-Path $testDataDir 'full-example.toml' - $result = Import-Toml -Path $path - $result.Data | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] - } - } - - Context 'Import-Toml - file content' { - It 'Import-Toml - reads all keys from full example file' { - $path = Join-Path $testDataDir 'full-example.toml' - $result = Import-Toml -Path $path - $result.Data['title'] | Should -Be 'TOML Example' - $result.Data['owner']['name'] | Should -Be 'Tom Preston-Werner' - $result.Data['database']['enabled'] | Should -Be $true - } - - It 'Import-Toml - reads integer file correctly' { - $path = Join-Path $testDataDir 'integers.toml' - $result = Import-Toml -Path $path - $result.Data['decimal'] | Should -Be 42 - $result.Data['negative'] | Should -Be -17 - $result.Data['hex'] | Should -Be 3735928559 # 0xDEADBEEF as unsigned long - } - - It 'Import-Toml - reads float file correctly' { - $path = Join-Path $testDataDir 'floats.toml' - $result = Import-Toml -Path $path - ([double]$result.Data['positive'] - 3.1415) | Should -BeLessThan 0.0001 - [double]::IsNaN($result.Data['special_nan']) | Should -BeTrue - } - - It 'Import-Toml - reads boolean file correctly' { - $path = Join-Path $testDataDir 'booleans.toml' - $result = Import-Toml -Path $path - $result.Data['enabled'] | Should -Be $true - $result.Data['disabled'] | Should -Be $false - } - - It 'Import-Toml - reads datetime file correctly' { - $path = Join-Path $testDataDir 'datetimes.toml' - $result = Import-Toml -Path $path - $result.Data['offset_dt_z'] | Should -BeOfType [System.DateTimeOffset] - $result.Data['local_date'] | Should -BeOfType [System.DateTime] - $result.Data['local_time'] | Should -BeOfType [System.TimeSpan] - } - - It 'Import-Toml - reads array file correctly' { - $path = Join-Path $testDataDir 'arrays.toml' - $result = Import-Toml -Path $path - $result.Data['integers'] | Should -HaveCount 3 - $result.Data['empty'] | Should -HaveCount 0 - } - - It 'Import-Toml - reads array-of-tables file correctly' { - $path = Join-Path $testDataDir 'array-of-tables.toml' - $result = Import-Toml -Path $path - $result.Data['products'] | Should -HaveCount 2 - $result.Data['products'][0]['name'] | Should -Be 'Hammer' - } - } - - Context 'Import-Toml - pipeline' { - It 'Import-Toml - accepts path from pipeline' { - $path = Join-Path $testDataDir 'booleans.toml' - $result = $path | Import-Toml - $result.GetType().Name | Should -Be 'TomlDocument' - } - } - - Context 'Import-Toml - error handling' { - It 'Import-Toml - throws when file does not exist' { - { Import-Toml -Path 'nonexistent-file-that-does-not-exist.toml' } | Should -Throw - } - - It 'Import-Toml - throws when file contains invalid TOML' { - $badToml = Join-Path $TestDrive 'bad.toml' - Set-Content -Path $badToml -Value 'invalid = = "bad"' - { Import-Toml -Path $badToml } | Should -Throw - } - } - } -} diff --git a/tests/Toml.Tests.ps1 b/tests/Toml.Tests.ps1 index c77ae33..1cbbcc3 100644 --- a/tests/Toml.Tests.ps1 +++ b/tests/Toml.Tests.ps1 @@ -1,24 +1,804 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester tests' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester tests' -)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Required for Pester tests')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Required for Pester tests')] [CmdletBinding()] param() -Describe 'Module' { +Describe 'Toml' { BeforeAll { - . (Join-Path $PSScriptRoot 'bootstrap.ps1') + $dataDir = Join-Path $PSScriptRoot 'data' } - It 'Module has expected commands' { - $commands = Get-Command -Module Toml | Select-Object -ExpandProperty Name | Sort-Object - $commands | Should -Contain 'ConvertFrom-Toml' - $commands | Should -Contain 'ConvertTo-Toml' - $commands | Should -Contain 'Import-Toml' - $commands | Should -Contain 'Export-Toml' + Context 'Module' { + It 'exposes the expected public commands' { + $commands = Get-Command -Module Toml | Select-Object -ExpandProperty Name | Sort-Object + $commands | Should -Contain 'ConvertFrom-Toml' + $commands | Should -Contain 'ConvertTo-Toml' + $commands | Should -Contain 'Import-Toml' + $commands | Should -Contain 'Export-Toml' + } + } + + Describe 'ConvertFrom-Toml' { + + Context 'Return type' { + It 'returns a TomlDocument for a minimal document' { + $result = ConvertFrom-Toml -InputObject 'key = "value"' + $result.GetType().Name | Should -Be 'TomlDocument' + } + + It 'Data property is an OrderedDictionary' { + $result = ConvertFrom-Toml -InputObject 'key = "value"' + $result.Data | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] + } + + It 'FilePath is null when parsed from string' { + $result = ConvertFrom-Toml -InputObject 'key = "value"' + $result.FilePath | Should -BeNullOrEmpty + } + + It 'accepts pipeline input' { + $result = 'key = "value"' | ConvertFrom-Toml + $result.GetType().Name | Should -Be 'TomlDocument' + } + } + + Context 'Strings' { + It 'parses a basic string' { + $result = ConvertFrom-Toml -InputObject 'key = "hello"' + $result.Data['key'] | Should -Be 'hello' + } + + It 'parses a literal string' { + $result = ConvertFrom-Toml -InputObject "key = 'C:\path'" + $result.Data['key'] | Should -Be 'C:\path' + } + + It 'parses escape sequences in a basic string' { + $result = ConvertFrom-Toml -InputObject 'key = "tab\there"' + $result.Data['key'] | Should -Be "tab`there" + } + + It 'parses escaped quotes in a basic string' { + $result = ConvertFrom-Toml -InputObject 'key = "say \"hi\""' + $result.Data['key'] | Should -Be 'say "hi"' + } + + It 'parses a Unicode \uXXXX escape sequence' { + $result = ConvertFrom-Toml -InputObject 'key = "\u03B1"' + $result.Data['key'] | Should -Be ([char]0x03B1).ToString() + } + + It 'parses a multi-line basic string' { + $toml = "key = `"`"`"`nline one`nline two`n`"`"`"" + $result = ConvertFrom-Toml -InputObject $toml + $result.Data['key'] | Should -Match 'line one' + $result.Data['key'] | Should -Match 'line two' + } + + It 'parses a multi-line literal string' { + $toml = "key = '''" + [System.Environment]::NewLine + "raw\nno escape" + [System.Environment]::NewLine + "'''" + $result = ConvertFrom-Toml -InputObject $toml + $result.Data['key'] | Should -Match 'raw\\nno escape' + } + + It 'parses an empty string' { + $result = ConvertFrom-Toml -InputObject 'key = ""' + $result.Data['key'] | Should -Be '' + } + + It 'parses strings fixture file' { + $result = ConvertFrom-Toml -InputObject (Get-Content (Join-Path $dataDir 'strings.toml') -Raw) + $result.Data['literal'] | Should -Be 'C:\Users\nodejs\templates' + $result.Data['empty'] | Should -Be '' + } + } + + Context 'Integers' { + It 'parses a decimal integer as [long]' { + $result = ConvertFrom-Toml -InputObject 'n = 42' + $result.Data['n'] | Should -Be 42 + $result.Data['n'] | Should -BeOfType [long] + } + + It 'parses a negative integer' { + $result = ConvertFrom-Toml -InputObject 'n = -17' + $result.Data['n'] | Should -Be -17 + } + + It 'parses zero' { + $result = ConvertFrom-Toml -InputObject 'n = 0' + $result.Data['n'] | Should -Be 0 + } + + It 'parses an integer with underscore separators' { + $result = ConvertFrom-Toml -InputObject 'n = 1_000_000' + $result.Data['n'] | Should -Be 1000000 + } + + It 'parses a hexadecimal integer' { + $result = ConvertFrom-Toml -InputObject 'n = 0xFF' + $result.Data['n'] | Should -Be 255 + } + + It 'parses an octal integer' { + $result = ConvertFrom-Toml -InputObject 'n = 0o17' + $result.Data['n'] | Should -Be 15 + } + + It 'parses a binary integer' { + $result = ConvertFrom-Toml -InputObject 'n = 0b1010' + $result.Data['n'] | Should -Be 10 + } + + It 'parses integers fixture file' { + $result = ConvertFrom-Toml -InputObject (Get-Content (Join-Path $dataDir 'integers.toml') -Raw) + $result.Data['decimal'] | Should -Be 42 + $result.Data['negative'] | Should -Be -17 + $result.Data['hex'] | Should -Be 3735928559 + $result.Data['octal'] | Should -Be 493 + $result.Data['binary'] | Should -Be 214 + } + } + + Context 'Floats' { + It 'parses a positive float as [double]' { + $result = ConvertFrom-Toml -InputObject 'n = 3.14' + $result.Data['n'] | Should -Be 3.14 + $result.Data['n'] | Should -BeOfType [double] + } + + It 'parses a negative float' { + $result = ConvertFrom-Toml -InputObject 'n = -0.01' + $result.Data['n'] | Should -Be -0.01 + } + + It 'parses scientific notation' { + $result = ConvertFrom-Toml -InputObject 'n = 5e+22' + $result.Data['n'] | Should -Be 5e22 + } + + It 'parses inf' { + $result = ConvertFrom-Toml -InputObject 'n = inf' + $result.Data['n'] | Should -Be ([double]::PositiveInfinity) + } + + It 'parses -inf' { + $result = ConvertFrom-Toml -InputObject 'n = -inf' + $result.Data['n'] | Should -Be ([double]::NegativeInfinity) + } + + It 'parses nan' { + $result = ConvertFrom-Toml -InputObject 'n = nan' + [double]::IsNaN($result.Data['n']) | Should -BeTrue + } + + It 'parses floats fixture file' { + $result = ConvertFrom-Toml -InputObject (Get-Content (Join-Path $dataDir 'floats.toml') -Raw) + ([double]$result.Data['positive'] - 3.1415) | Should -BeLessThan 0.0001 + ([double]$result.Data['negative'] - (-0.01)) | Should -BeLessThan 0.0001 + } + } + + Context 'Booleans' { + It 'parses true as [bool]' { + $result = ConvertFrom-Toml -InputObject 'flag = true' + $result.Data['flag'] | Should -Be $true + $result.Data['flag'] | Should -BeOfType [bool] + } + + It 'parses false as [bool]' { + $result = ConvertFrom-Toml -InputObject 'flag = false' + $result.Data['flag'] | Should -Be $false + } + } + + Context 'Datetimes' { + It 'parses an offset datetime with Z suffix as DateTimeOffset' { + $result = ConvertFrom-Toml -InputObject 'dt = 1979-05-27T07:32:00Z' + $result.Data['dt'] | Should -BeOfType [System.DateTimeOffset] + ([System.DateTimeOffset]$result.Data['dt']).Year | Should -Be 1979 + ([System.DateTimeOffset]$result.Data['dt']).Day | Should -Be 27 + } + + It 'parses an offset datetime with +HH:MM offset' { + $result = ConvertFrom-Toml -InputObject 'dt = 1979-05-27T07:32:00+05:30' + $result.Data['dt'] | Should -BeOfType [System.DateTimeOffset] + ([System.DateTimeOffset]$result.Data['dt']).Offset.Hours | Should -Be 5 + ([System.DateTimeOffset]$result.Data['dt']).Offset.Minutes | Should -Be 30 + } + + It 'parses an offset datetime with space separator' { + $result = ConvertFrom-Toml -InputObject 'dt = 1987-07-05 17:45:00Z' + $result.Data['dt'] | Should -BeOfType [System.DateTimeOffset] + ([System.DateTimeOffset]$result.Data['dt']).Year | Should -Be 1987 + } + + It 'parses a local datetime as DateTime with Unspecified kind' { + $result = ConvertFrom-Toml -InputObject 'dt = 1979-05-27T07:32:00' + $result.Data['dt'] | Should -BeOfType [System.DateTime] + ([System.DateTime]$result.Data['dt']).Kind | Should -Be ([System.DateTimeKind]::Unspecified) + } + + It 'parses a local date as DateTime with zero time' { + $result = ConvertFrom-Toml -InputObject 'dt = 1979-05-27' + $result.Data['dt'] | Should -BeOfType [System.DateTime] + $dt = [System.DateTime]$result.Data['dt'] + $dt.Year | Should -Be 1979 + $dt.Month | Should -Be 5 + $dt.Day | Should -Be 27 + $dt.Hour | Should -Be 0 + } + + It 'parses a local time as TimeSpan' { + $result = ConvertFrom-Toml -InputObject 'tm = 07:32:00' + $result.Data['tm'] | Should -BeOfType [System.TimeSpan] + ([System.TimeSpan]$result.Data['tm']).Hours | Should -Be 7 + ([System.TimeSpan]$result.Data['tm']).Minutes | Should -Be 32 + } + + It 'parses datetimes fixture file' { + $result = ConvertFrom-Toml -InputObject (Get-Content (Join-Path $dataDir 'datetimes.toml') -Raw) + $result.Data['offset_dt_z'] | Should -BeOfType [System.DateTimeOffset] + $result.Data['offset_dt_num'] | Should -BeOfType [System.DateTimeOffset] + $result.Data['local_dt'] | Should -BeOfType [System.DateTime] + $result.Data['local_date'] | Should -BeOfType [System.DateTime] + $result.Data['local_time'] | Should -BeOfType [System.TimeSpan] + } + } + + Context 'Arrays' { + It 'parses an integer array' { + $result = ConvertFrom-Toml -InputObject 'arr = [1, 2, 3]' + $result.Data['arr'] | Should -HaveCount 3 + $result.Data['arr'][0] | Should -Be 1 + $result.Data['arr'][2] | Should -Be 3 + } + + It 'parses a string array' { + $result = ConvertFrom-Toml -InputObject 'arr = ["a", "b", "c"]' + $result.Data['arr'][0] | Should -Be 'a' + $result.Data['arr'][1] | Should -Be 'b' + } + + It 'parses an empty array' { + $result = ConvertFrom-Toml -InputObject 'arr = []' + $result.Data['arr'] | Should -HaveCount 0 + } + + It 'parses a nested array' { + $result = ConvertFrom-Toml -InputObject 'arr = [[1, 2], [3, 4]]' + $result.Data['arr'] | Should -HaveCount 2 + $result.Data['arr'][0][1] | Should -Be 2 + } + + It 'parses a multiline array' { + $result = ConvertFrom-Toml -InputObject "arr = [`n 1,`n 2,`n 3,`n]" + $result.Data['arr'] | Should -HaveCount 3 + } + + It 'parses arrays fixture file' { + $result = ConvertFrom-Toml -InputObject (Get-Content (Join-Path $dataDir 'arrays.toml') -Raw) + $result.Data['integers'] | Should -HaveCount 3 + $result.Data['strings'] | Should -HaveCount 3 + $result.Data['empty'] | Should -HaveCount 0 + $result.Data['nested'] | Should -HaveCount 2 + } + } + + Context 'Tables' { + It 'parses a standard table' { + $result = ConvertFrom-Toml -InputObject "[server]`nhost = `"localhost`"" + $result.Data['server'] | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] + $result.Data['server']['host'] | Should -Be 'localhost' + } + + It 'parses an inline table' { + $result = ConvertFrom-Toml -InputObject 'point = { x = 1, y = 2 }' + $result.Data['point']['x'] | Should -Be 1 + $result.Data['point']['y'] | Should -Be 2 + } + + It 'parses deeply nested tables via dotted header' { + $result = ConvertFrom-Toml -InputObject "[a.b.c]`nkey = `"deep`"" + $result.Data['a']['b']['c']['key'] | Should -Be 'deep' + } + + It 'parses tables fixture file' { + $result = ConvertFrom-Toml -InputObject (Get-Content (Join-Path $dataDir 'tables.toml') -Raw) + $result.Data['simple']['key'] | Should -Be 'value' + $result.Data['dotted']['key']['works'] | Should -Be $true + $result.Data['inline_parent']['inline']['one'] | Should -Be 1 + } + } + + Context 'Array of tables' { + It 'parses array of tables from fixture file' { + $result = ConvertFrom-Toml -InputObject (Get-Content (Join-Path $dataDir 'array-of-tables.toml') -Raw) + $result.Data['products'] | Should -HaveCount 2 + $result.Data['products'][0]['name'] | Should -Be 'Hammer' + $result.Data['products'][1]['name'] | Should -Be 'Nail' + } + + It 'parses nested array of tables' { + $result = ConvertFrom-Toml -InputObject (Get-Content (Join-Path $dataDir 'array-of-tables.toml') -Raw) + $result.Data['fruits'][0]['varieties'] | Should -HaveCount 2 + $result.Data['fruits'][0]['varieties'][0]['name'] | Should -Be 'red delicious' + } + + It 'parses sub-tables independently per array entry' { + $toml = "[[stages]]`nname = `"build`"`n`n [stages.env]`n KEY = `"A`"`n`n[[stages]]`nname = `"test`"`n`n [stages.env]`n KEY = `"B`"" + $result = ConvertFrom-Toml -InputObject $toml + $result.Data['stages'][0]['env']['KEY'] | Should -Be 'A' + $result.Data['stages'][1]['env']['KEY'] | Should -Be 'B' + } + } + + Context 'Keys' { + It 'parses a bare key' { + $result = ConvertFrom-Toml -InputObject 'bare_key = "value"' + $result.Data['bare_key'] | Should -Be 'value' + } + + It 'parses a quoted key with spaces' { + $result = ConvertFrom-Toml -InputObject '"quoted key" = "value"' + $result.Data['quoted key'] | Should -Be 'value' + } + + It 'parses a dotted key into nested tables' { + $result = ConvertFrom-Toml -InputObject 'a.b = "value"' + $result.Data['a']['b'] | Should -Be 'value' + } + + It 'preserves key insertion order' { + $result = ConvertFrom-Toml -InputObject "z = 1`ny = 2`nx = 3" + $keys = $result.Data.Keys + $keys[0] | Should -Be 'z' + $keys[1] | Should -Be 'y' + $keys[2] | Should -Be 'x' + } + } + + Context 'Comments and whitespace' { + It 'ignores inline comments' { + $result = ConvertFrom-Toml -InputObject 'key = "value" # comment' + $result.Data['key'] | Should -Be 'value' + } + + It 'ignores full-line comments' { + $result = ConvertFrom-Toml -InputObject "# comment`nkey = `"value`"" + $result.Data['key'] | Should -Be 'value' + } + + It 'handles CRLF line endings' { + $result = ConvertFrom-Toml -InputObject "a = 1`r`nb = 2" + $result.Data['a'] | Should -Be 1 + $result.Data['b'] | Should -Be 2 + } + } + + Context 'Full example' { + It 'parses the full example fixture file' { + $result = ConvertFrom-Toml -InputObject (Get-Content (Join-Path $dataDir 'full-example.toml') -Raw) + $result.Data['title'] | Should -Be 'TOML Example' + $result.Data['owner']['name'] | Should -Be 'Tom Preston-Werner' + $result.Data['database']['enabled'] | Should -Be $true + $result.Data['database']['ports'] | Should -HaveCount 3 + $result.Data['servers']['alpha']['ip'] | Should -Be '10.0.0.1' + $result.Data['servers']['beta']['role'] | Should -Be 'backend' + } + } + + Context 'Advanced fixtures' { + It 'parses ci-pipeline.toml' { + $result = ConvertFrom-Toml -InputObject (Get-Content (Join-Path $dataDir 'advanced\ci-pipeline.toml') -Raw) + $result.Data['pipeline']['name'] | Should -Be 'my-app-ci' + $result.Data['pipeline']['stages'] | Should -HaveCount 3 + $result.Data['pipeline']['stages'][0]['steps'] | Should -HaveCount 2 + } + + It 'parses numbers-and-datetimes.toml' { + $result = ConvertFrom-Toml -InputObject (Get-Content (Join-Path $dataDir 'advanced\numbers-and-datetimes.toml') -Raw) + $result.Data['integers']['hex-upper'] | Should -Be 3735928559 + $result.Data['floats']['infinity'] | Should -Be ([double]::PositiveInfinity) + $result.Data['datetimes']['utc-space-sep'] | Should -BeOfType [System.DateTimeOffset] + $result.Data['arrays']['deeply-nested'] | Should -HaveCount 2 + } + + It 'parses app-config.toml' { + $result = ConvertFrom-Toml -InputObject (Get-Content (Join-Path $dataDir 'advanced\app-config.toml') -Raw) + $result.Data['app']['version'] | Should -Be '2.4.1' + $result.Data['feature_flag'] | Should -HaveCount 3 + $result.Data['feature_flag'][0]['targeting']['segments'] | Should -Contain 'beta-users' + } + + It 'parses cargo-manifest.toml' { + $result = ConvertFrom-Toml -InputObject (Get-Content (Join-Path $dataDir 'advanced\cargo-manifest.toml') -Raw) + $result.Data['package']['name'] | Should -Be 'my-awesome-lib' + $result.Data['package']['limits']['max-size-binary'] | Should -Be 1048576 + $result.Data['bench'] | Should -HaveCount 3 + } + + It 'parses database-server.toml' { + $result = ConvertFrom-Toml -InputObject (Get-Content (Join-Path $dataDir 'advanced\database-server.toml') -Raw) + $result.Data['cluster']['name'] | Should -Be 'primary-pg-cluster' + $result.Data['nodes']['replicas'] | Should -HaveCount 2 + $result.Data['nodes']['replicas'][0]['slots'] | Should -HaveCount 2 + } + } + + Context 'Error handling' { + It 'throws on invalid TOML syntax' { + { ConvertFrom-Toml -InputObject 'invalid = = "bad"' } | Should -Throw + } + + It 'throws on a duplicate key' { + { ConvertFrom-Toml -InputObject "key = 1`nkey = 2" } | Should -Throw + } + + It 'throws on a missing value' { + { ConvertFrom-Toml -InputObject 'key =' } | Should -Throw + } + + It 'throws on empty string input' { + { ConvertFrom-Toml -InputObject '' } | Should -Throw + } + + It 'throws on table redefinition' { + { ConvertFrom-Toml -InputObject "[a]`nkey = 1`n[a]`nother = 2" } | Should -Throw + } + } + } + + Describe 'ConvertTo-Toml' { + + Context 'Return type' { + It 'returns a string' { + $result = ConvertTo-Toml -InputObject ([ordered]@{ key = 'value' }) + $result | Should -BeOfType [string] + } + + It 'result is non-empty for non-empty input' { + $result = ConvertTo-Toml -InputObject ([ordered]@{ key = 'value' }) + $result | Should -Not -BeNullOrEmpty + } + + It 'accepts pipeline input' { + $result = [ordered]@{ key = 'value' } | ConvertTo-Toml + $result | Should -BeOfType [string] + } + } + + Context 'Scalar serialization' { + It 'serializes a string value' { + $result = ConvertTo-Toml -InputObject ([ordered]@{ title = 'Hello' }) + $result | Should -Match 'title = "Hello"' + } + + It 'serializes an integer value' { + $result = ConvertTo-Toml -InputObject ([ordered]@{ n = [long]42 }) + $result | Should -Match 'n = 42' + } + + It 'serializes true and false' { + ConvertTo-Toml -InputObject ([ordered]@{ flag = $true }) | Should -Match 'flag = true' + ConvertTo-Toml -InputObject ([ordered]@{ flag = $false }) | Should -Match 'flag = false' + } + + It 'serializes a float value' { + $result = ConvertTo-Toml -InputObject ([ordered]@{ pi = 3.14 }) + $result | Should -Match '3\.14' + } + + It 'serializes a DateTimeOffset' { + $dto = [System.DateTimeOffset]::new(1979, 5, 27, 7, 32, 0, [System.TimeSpan]::Zero) + ConvertTo-Toml -InputObject ([ordered]@{ dt = $dto }) | Should -Match '1979-05-27' + } + + It 'serializes a DateTime (local datetime)' { + $dt = [System.DateTime]::new(2024, 6, 1, 15, 30, 0, [System.DateTimeKind]::Unspecified) + ConvertTo-Toml -InputObject ([ordered]@{ dt = $dt }) | Should -Match '2024-06-01' + } + + It 'serializes a TimeSpan as local time' { + $ts = [System.TimeSpan]::new(0, 8, 30, 0) + ConvertTo-Toml -InputObject ([ordered]@{ tm = $ts }) | Should -Match '08:30:00' + } + } + + Context 'Nested tables and arrays' { + It 'serializes a nested ordered dict as a TOML table header' { + $result = ConvertTo-Toml -InputObject ([ordered]@{ + server = [ordered]@{ host = 'localhost'; port = [long]8080 } + }) + $result | Should -Match '\[server\]' + $result | Should -Match 'host = "localhost"' + } + + It 'serializes an array of integers' { + $result = ConvertTo-Toml -InputObject ([ordered]@{ ports = @([long]80, [long]443) }) + $result | Should -Match '80' + $result | Should -Match '443' + } + + It 'serializes a PSCustomObject' { + $result = ConvertTo-Toml -InputObject ([PSCustomObject]@{ name = 'Alice'; age = [long]30 }) + $result | Should -Match 'name = "Alice"' + $result | Should -Match 'age = 30' + } + + It 'accepts a TomlDocument as input' { + $doc = ConvertFrom-Toml -InputObject "title = `"Test`"`n[section]`nvalue = 42" + $result = ConvertTo-Toml -InputObject $doc + $result | Should -Match 'title = "Test"' + $result | Should -Match '\[section\]' + } + } + + Context 'Round-trip' { + It 'string and integer values survive a round-trip' { + $toml = ConvertTo-Toml -InputObject ([ordered]@{ key = 'value' }) + (ConvertFrom-Toml -InputObject $toml).Data['key'] | Should -Be 'value' + } + + It 'integer value survives a round-trip' { + $toml = ConvertTo-Toml -InputObject ([ordered]@{ n = [long]12345 }) + (ConvertFrom-Toml -InputObject $toml).Data['n'] | Should -Be 12345 + } + + It 'float value survives a round-trip' { + $toml = ConvertTo-Toml -InputObject ([ordered]@{ n = 2.718 }) + ([double](ConvertFrom-Toml -InputObject $toml).Data['n'] - 2.718) | Should -BeLessThan 0.001 + } + + It 'DateTimeOffset survives a round-trip' { + $dto = [System.DateTimeOffset]::new(2024, 3, 15, 12, 0, 0, [System.TimeSpan]::FromHours(2)) + $toml = ConvertTo-Toml -InputObject ([ordered]@{ dt = $dto }) + $rt = (ConvertFrom-Toml -InputObject $toml).Data['dt'] + $rt | Should -BeOfType [System.DateTimeOffset] + ([System.DateTimeOffset]$rt).Year | Should -Be 2024 + } + + It 'nested table survives a round-trip' { + $toml = ConvertTo-Toml -InputObject ([ordered]@{ + database = [ordered]@{ host = 'db.example.com'; port = [long]5432 } + }) + $rt = (ConvertFrom-Toml -InputObject $toml).Data + $rt['database']['host'] | Should -Be 'db.example.com' + $rt['database']['port'] | Should -Be 5432 + } + + It 'integer array survives a round-trip' { + $toml = ConvertTo-Toml -InputObject ([ordered]@{ nums = @([long]1, [long]2, [long]3) }) + $rt = (ConvertFrom-Toml -InputObject $toml).Data['nums'] + $rt | Should -HaveCount 3 + $rt[0] | Should -Be 1 + } + + It 'full example fixture file survives a round-trip' { + $original = ConvertFrom-Toml -InputObject (Get-Content (Join-Path $dataDir 'full-example.toml') -Raw) + $rt = ConvertFrom-Toml -InputObject (ConvertTo-Toml -InputObject $original) + $rt.Data['title'] | Should -Be 'TOML Example' + $rt.Data['owner']['name'] | Should -Be 'Tom Preston-Werner' + $rt.Data['database']['enabled'] | Should -Be $true + $rt.Data['database']['ports'] | Should -HaveCount 3 + $rt.Data['servers']['alpha']['ip'] | Should -Be '10.0.0.1' + } + } + + Context 'Error handling' { + It 'throws on null input' { + { ConvertTo-Toml -InputObject $null } | Should -Throw + } + } + } + + Describe 'Import-Toml' { + + Context 'Return type' { + It 'returns a TomlDocument' { + $result = Import-Toml -Path (Join-Path $dataDir 'full-example.toml') + $result.GetType().Name | Should -Be 'TomlDocument' + } + + It 'sets FilePath to the resolved absolute path' { + $result = Import-Toml -Path (Join-Path $dataDir 'full-example.toml') + $result.FilePath | Should -Not -BeNullOrEmpty + [System.IO.Path]::IsPathRooted($result.FilePath) | Should -BeTrue + } + + It 'Data is an OrderedDictionary' { + $result = Import-Toml -Path (Join-Path $dataDir 'full-example.toml') + $result.Data | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] + } + } + + Context 'File content' { + It 'reads the full example fixture file' { + $result = Import-Toml -Path (Join-Path $dataDir 'full-example.toml') + $result.Data['title'] | Should -Be 'TOML Example' + $result.Data['owner']['name'] | Should -Be 'Tom Preston-Werner' + $result.Data['database']['enabled'] | Should -Be $true + } + + It 'reads integers fixture file' { + $result = Import-Toml -Path (Join-Path $dataDir 'integers.toml') + $result.Data['decimal'] | Should -Be 42 + $result.Data['negative'] | Should -Be -17 + $result.Data['hex'] | Should -Be 3735928559 + } + + It 'reads floats fixture file' { + $result = Import-Toml -Path (Join-Path $dataDir 'floats.toml') + ([double]$result.Data['positive'] - 3.1415) | Should -BeLessThan 0.0001 + [double]::IsNaN($result.Data['special_nan']) | Should -BeTrue + } + + It 'reads booleans fixture file' { + $result = Import-Toml -Path (Join-Path $dataDir 'booleans.toml') + $result.Data['enabled'] | Should -Be $true + $result.Data['disabled'] | Should -Be $false + } + + It 'reads datetimes fixture file' { + $result = Import-Toml -Path (Join-Path $dataDir 'datetimes.toml') + $result.Data['offset_dt_z'] | Should -BeOfType [System.DateTimeOffset] + $result.Data['local_date'] | Should -BeOfType [System.DateTime] + $result.Data['local_time'] | Should -BeOfType [System.TimeSpan] + } + + It 'reads arrays fixture file' { + $result = Import-Toml -Path (Join-Path $dataDir 'arrays.toml') + $result.Data['integers'] | Should -HaveCount 3 + $result.Data['empty'] | Should -HaveCount 0 + } + + It 'reads array-of-tables fixture file' { + $result = Import-Toml -Path (Join-Path $dataDir 'array-of-tables.toml') + $result.Data['products'] | Should -HaveCount 2 + $result.Data['products'][0]['name'] | Should -Be 'Hammer' + } + } + + Context 'Pipeline' { + It 'accepts a path from the pipeline' { + $result = (Join-Path $dataDir 'booleans.toml') | Import-Toml + $result.GetType().Name | Should -Be 'TomlDocument' + } + } + + Context 'Error handling' { + It 'throws when the file does not exist' { + { Import-Toml -Path 'nonexistent-file.toml' } | Should -Throw + } + + It 'throws when the file contains invalid TOML' { + $bad = Join-Path $TestDrive 'bad.toml' + Set-Content -Path $bad -Value 'invalid = = "bad"' + { Import-Toml -Path $bad } | Should -Throw + } + } + } + + Describe 'Export-Toml' { + + Context 'Basic write' { + It 'creates a file at the given path' { + $path = Join-Path $TestDrive 'output.toml' + Export-Toml -InputObject ([ordered]@{ key = 'value' }) -Path $path + Test-Path -Path $path | Should -BeTrue + } + + It 'file content is valid TOML' { + $path = Join-Path $TestDrive 'content.toml' + Export-Toml -InputObject ([ordered]@{ title = 'Hello' }) -Path $path + Get-Content -Path $path -Raw | Should -Match 'title = "Hello"' + } + + It 'returns nothing to the output stream' { + $path = Join-Path $TestDrive 'void.toml' + $result = Export-Toml -InputObject ([ordered]@{ k = 'v' }) -Path $path + $result | Should -BeNullOrEmpty + } + + It 'creates parent directories when they do not exist' { + $path = Join-Path $TestDrive 'nested\subdir\output.toml' + Export-Toml -InputObject ([ordered]@{ k = 'v' }) -Path $path + Test-Path -Path $path | Should -BeTrue + } + } + + Context 'Round-trip via Import-Toml' { + It 'preserves string values' { + $path = Join-Path $TestDrive 'rt-string.toml' + Export-Toml -InputObject ([ordered]@{ greeting = 'Hello World' }) -Path $path + (Import-Toml -Path $path).Data['greeting'] | Should -Be 'Hello World' + } + + It 'preserves integer values' { + $path = Join-Path $TestDrive 'rt-int.toml' + Export-Toml -InputObject ([ordered]@{ count = [long]42 }) -Path $path + (Import-Toml -Path $path).Data['count'] | Should -Be 42 + } + + It 'preserves boolean values' { + $path = Join-Path $TestDrive 'rt-bool.toml' + Export-Toml -InputObject ([ordered]@{ enabled = $true; disabled = $false }) -Path $path + $rt = (Import-Toml -Path $path).Data + $rt['enabled'] | Should -Be $true + $rt['disabled'] | Should -Be $false + } + + It 'preserves float values' { + $path = Join-Path $TestDrive 'rt-float.toml' + Export-Toml -InputObject ([ordered]@{ pi = 3.14159 }) -Path $path + ([double](Import-Toml -Path $path).Data['pi'] - 3.14159) | Should -BeLessThan 0.00001 + } + + It 'preserves nested tables' { + $path = Join-Path $TestDrive 'rt-nested.toml' + Export-Toml -InputObject ([ordered]@{ server = [ordered]@{ host = 'localhost'; port = [long]8080 } }) -Path $path + $rt = (Import-Toml -Path $path).Data + $rt['server']['host'] | Should -Be 'localhost' + $rt['server']['port'] | Should -Be 8080 + } + + It 'preserves integer arrays' { + $path = Join-Path $TestDrive 'rt-array.toml' + Export-Toml -InputObject ([ordered]@{ ports = @([long]80, [long]443) }) -Path $path + $rt = (Import-Toml -Path $path).Data['ports'] + $rt | Should -HaveCount 2 + $rt[0] | Should -Be 80 + } + + It 'full file survives a round-trip' { + $outPath = Join-Path $TestDrive 'rt-full.toml' + Export-Toml -InputObject (Import-Toml -Path (Join-Path $dataDir 'full-example.toml')) -Path $outPath + $rt = (Import-Toml -Path $outPath).Data + $rt['title'] | Should -Be 'TOML Example' + $rt['owner']['name'] | Should -Be 'Tom Preston-Werner' + $rt['database']['enabled'] | Should -Be $true + $rt['servers']['alpha']['ip'] | Should -Be '10.0.0.1' + } + } + + Context 'UTF-8 encoding' { + It 'writes UTF-8 without BOM' { + $path = Join-Path $TestDrive 'utf8.toml' + Export-Toml -InputObject ([ordered]@{ key = 'αβγ' }) -Path $path + $bytes = [System.IO.File]::ReadAllBytes($path) + if ($bytes.Length -ge 3) { + ($bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) | Should -BeFalse + } + } + + It 'round-trips Unicode string content correctly' { + $path = Join-Path $TestDrive 'unicode.toml' + Export-Toml -InputObject ([ordered]@{ msg = 'こんにちは' }) -Path $path + (Import-Toml -Path $path).Data['msg'] | Should -Be 'こんにちは' + } + } + + Context 'Pipeline input' { + It 'accepts a TomlDocument from the pipeline' { + $outPath = Join-Path $TestDrive 'from-pipeline.toml' + Import-Toml -Path (Join-Path $dataDir 'booleans.toml') | Export-Toml -Path $outPath + Test-Path $outPath | Should -BeTrue + (Import-Toml -Path $outPath).Data['enabled'] | Should -Be $true + } + } + + Context 'WhatIf' { + It 'does not create a file when -WhatIf is passed' { + $path = Join-Path $TestDrive 'whatif.toml' + Export-Toml -InputObject ([ordered]@{ k = 'v' }) -Path $path -WhatIf + Test-Path -Path $path | Should -BeFalse + } + } + + Context 'Error handling' { + It 'throws on null InputObject' { + { Export-Toml -InputObject $null -Path (Join-Path $TestDrive 'null.toml') } | Should -Throw + } + } } } diff --git a/tests/bootstrap.ps1 b/tests/bootstrap.ps1 deleted file mode 100644 index 41f1bd8..0000000 --- a/tests/bootstrap.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -<# - .SYNOPSIS - Bootstrap helper for local Pester runs. - - .DESCRIPTION - Imports the built Toml module from ./output/Toml/ before tests run. - Call from a BeforeAll block in every test file. -#> -[CmdletBinding()] -param() - -$outputManifest = Join-Path $PSScriptRoot '..' 'output' 'Toml' 'Toml.psd1' -if (-not (Test-Path $outputManifest)) { - throw "Module manifest not found at '$outputManifest'. Run build.ps1 first." -} - -Import-Module $outputManifest -Force -ErrorAction Stop From 6af7f930e8270cee5b30a44e721f11cad1060a45 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 23 Jul 2026 20:05:51 +0200 Subject: [PATCH 07/13] chore: Restore zensical.toml to .github/ Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/zensical.toml | 69 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/zensical.toml diff --git a/.github/zensical.toml b/.github/zensical.toml new file mode 100644 index 0000000..34d60b7 --- /dev/null +++ b/.github/zensical.toml @@ -0,0 +1,69 @@ +[project] +site_name = "Toml" +repo_name = "PSModule/Toml" +repo_url = "https://github.com/PSModule/Toml" + +[project.theme] +variant = "classic" +language = "en" +logo = "Assets/icon.png" +favicon = "Assets/icon.png" +features = [ + "navigation.instant", + "navigation.instant.progress", + "navigation.indexes", + "navigation.top", + "navigation.tracking", + "navigation.expand", + "search.suggest", + "search.highlight", + "content.code.copy" +] + +[[project.theme.palette]] +media = "(prefers-color-scheme)" +toggle.icon = "lucide/sun-moon" +toggle.name = "Switch to dark mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: dark)" +scheme = "slate" +primary = "black" +accent = "green" +toggle.icon = "lucide/moon" +toggle.name = "Switch to light mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: light)" +scheme = "default" +primary = "indigo" +accent = "green" +toggle.icon = "lucide/sun" +toggle.name = "Switch to system preference" + +[project.theme.icon] +repo = "fontawesome/brands/github" + +[project.markdown_extensions.toc] +permalink = true + +[project.markdown_extensions.attr_list] +[project.markdown_extensions.admonition] +[project.markdown_extensions.md_in_html] +[project.markdown_extensions.pymdownx.details] +[project.markdown_extensions.pymdownx.superfences] + +[[project.extra.social]] +icon = "fontawesome/brands/discord" +link = "https://discord.gg/jedJWCPAhD" +name = "PSModule on Discord" + +[[project.extra.social]] +icon = "fontawesome/brands/github" +link = "https://github.com/PSModule/" +name = "PSModule on GitHub" + +[project.extra.consent] +title = "Cookie consent" +description = "We use cookies to recognize your repeated visits and preferences, as well as to measure the effectiveness of our documentation and whether users find what they're searching for. With your consent, you're helping us to make our documentation better." +actions = ["accept", "reject"] From 9f42c39d361ff8e70e50b8d4d284648f505a25ff Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 23 Jul 2026 20:31:10 +0200 Subject: [PATCH 08/13] chore: Remove mkdocs.yml replaced by zensical.toml Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/mkdocs.yml | 81 ---------------------------------------------- 1 file changed, 81 deletions(-) delete mode 100644 .github/mkdocs.yml diff --git a/.github/mkdocs.yml b/.github/mkdocs.yml deleted file mode 100644 index 67bd37a..0000000 --- a/.github/mkdocs.yml +++ /dev/null @@ -1,81 +0,0 @@ -# The '-{{ ... }}-'' are variables that are replaced during deployment, so no need to change. -# Rest of the settings can be changed as per your requirements. -# -# References: -# - https://squidfunk.github.io/mkdocs-material/setup/ - -site_name: -{{ REPO_NAME }}- -theme: - name: material - language: en - font: - text: Roboto - code: Sono - logo: Assets/icon.png - favicon: Assets/icon.png - palette: - # Palette toggle for automatic mode - - media: "(prefers-color-scheme)" - toggle: - icon: material/link - name: Switch to dark mode - # Palette toggle for dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - toggle: - primary: black - accent: green - icon: material/toggle-switch-off-outline - name: Switch to light mode - # Palette toggle for light mode - - media: '(prefers-color-scheme: light)' - scheme: default - toggle: - primary: indigo - accent: green - icon: material/toggle-switch - name: Switch to system preference - icon: - repo: material/github - features: - - navigation.instant - - navigation.instant.progress - - navigation.indexes - - navigation.top - - navigation.tracking - - navigation.expand - - search.suggest - - search.highlight - -repo_name: -{{ REPO_OWNER }}-/-{{ REPO_NAME }}- -repo_url: https://github.com/-{{ REPO_OWNER }}-/-{{ REPO_NAME }}- - -plugins: - - search - -markdown_extensions: - - toc: - permalink: true # Adds a link icon to headings - - attr_list - - admonition - - md_in_html - - pymdownx.details # Enables collapsible admonitions - -extra: - social: - - icon: fontawesome/brands/discord - link: https://discord.gg/jedJWCPAhD - name: -{{ REPO_OWNER }}- on Discord - - icon: fontawesome/brands/github - link: https://github.com/-{{ REPO_OWNER }}-/ - name: -{{ REPO_OWNER }}- on GitHub - consent: - title: Cookie consent - description: >- - We use cookies to recognize your repeated visits and preferences, as well - as to measure the effectiveness of our documentation and whether users - find what they're searching for. With your consent, you're helping us to - make our documentation better. - actions: - - accept - - reject From a6a32c75a15123e3d0f2426d1e0cbee2808bb0df Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 24 Jul 2026 03:24:17 +0200 Subject: [PATCH 09/13] style: Align with MSXOrg and PSModule coding standards Code violations fixed: - [bool] parameters replaced with [switch] in Add-TomlTableText and Get-TomlBareToken; callers updated (-StopAtEquals) - \Continue = 'Stop' injected into built psm1 via build.ps1 - Skip-TomlWhitespace gains [OutputType([void])] Comment-based help: - ConvertFrom-Toml and ConvertTo-Toml: add .DESCRIPTION, .EXAMPLE x2, .INPUTS, .OUTPUTS, .NOTES to match required section order - All 20 private helpers: add .DESCRIPTION, .EXAMPLE, .INPUTS, .OUTPUTS - Add Write-Verbose to all four public functions Structure: - Add tests/BeforeAll.ps1 (shared setup entry-point per PSModule standard) Examples: - Add examples/data/ with three comment-free TOML fixtures 120/120 tests pass. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- build.ps1 | 3 + examples/data/cargo-manifest.toml | 91 +++++++++++ examples/data/ci-pipeline.toml | 71 +++++++++ examples/data/database-server.toml | 147 ++++++++++++++++++ src/functions/private/Add-TomlTableText.ps1 | 18 ++- .../ConvertFrom-TomlBasicStringValue.ps1 | 17 ++ .../private/ConvertFrom-TomlDateTime.ps1 | 16 ++ .../ConvertFrom-TomlLiteralStringValue.ps1 | 17 ++ .../private/ConvertFrom-TomlParsedValue.ps1 | 26 +++- .../private/ConvertFrom-TomlScalarToken.ps1 | 21 +++ .../private/ConvertFrom-TomlTable.ps1 | 17 ++ .../private/ConvertFrom-TomlValue.ps1 | 21 +++ .../private/ConvertTo-TomlArrayObject.ps1 | 16 ++ .../private/ConvertTo-TomlTableObject.ps1 | 17 ++ src/functions/private/ConvertTo-TomlValue.ps1 | 23 +++ src/functions/private/Format-TomlKey.ps1 | 21 +++ src/functions/private/Get-TomlBareToken.ps1 | 20 ++- .../private/Get-TomlContentWithoutComment.ps1 | 16 ++ src/functions/private/Get-TomlNestedTable.ps1 | 19 +++ src/functions/private/Join-TomlKeyPath.ps1 | 16 ++ src/functions/private/Skip-TomlWhitespace.ps1 | 18 +++ src/functions/private/Split-TomlDottedKey.ps1 | 22 +++ .../Test-TomlEndsWithDoubleNewLine.ps1 | 18 +++ src/functions/private/Test-TomlTableArray.ps1 | 25 +++ src/functions/public/ConvertFrom-Toml.ps1 | 38 +++++ src/functions/public/ConvertTo-Toml.ps1 | 43 +++++ src/functions/public/Export-Toml.ps1 | 2 + src/functions/public/Import-Toml.ps1 | 2 + tests/BeforeAll.ps1 | 3 + 29 files changed, 781 insertions(+), 3 deletions(-) create mode 100644 examples/data/cargo-manifest.toml create mode 100644 examples/data/ci-pipeline.toml create mode 100644 examples/data/database-server.toml create mode 100644 tests/BeforeAll.ps1 diff --git a/build.ps1 b/build.ps1 index b702126..8b07cf2 100644 --- a/build.ps1 +++ b/build.ps1 @@ -34,6 +34,9 @@ $sb = [System.Text.StringBuilder]::new() $headerPath = Join-Path $srcPath 'header.ps1' if (Test-Path $headerPath) { $null = $sb.AppendLine((Get-Content $headerPath -Raw)) +} else { + $null = $sb.AppendLine('$ErrorActionPreference = ''Stop''') + $null = $sb.AppendLine() } # init diff --git a/examples/data/cargo-manifest.toml b/examples/data/cargo-manifest.toml new file mode 100644 index 0000000..391002b --- /dev/null +++ b/examples/data/cargo-manifest.toml @@ -0,0 +1,91 @@ +[package] +name = "my-awesome-lib" +version = "0.9.1" +edition = "2021" +authors = ["Alice ", "Bob "] +license = "MIT OR Apache-2.0" +description = "A library demonstrating TOML 1.0.0 features." +readme = "README.md" + +package.metadata.docs_rs.features = ["full"] +package.metadata.docs_rs.targets = ["x86_64-unknown-linux-gnu"] + +package.metadata.build_notes = ''' +Build with: + cargo build --release + +Windows cross-compile: + cargo build --target x86_64-pc-windows-gnu \ + --features "tls-native" +No escapes needed: C:\Users\Alice\project is literal. +''' + +[features] +default = ["std", "derive"] +full = ["std", "derive", "async", "serde-support"] +std = [] +derive = [] +async = [] +serde-support = [] + +[lib] +name = "my_awesome_lib" +crate-type = ["rlib", "cdylib"] + +[package.limits] +max-size-decimal = 1_048_576 +max-size-hex = 0x0010_0000 +max-size-octal = 0o4_000_000 +max-size-binary = 0b0001_0000_0000_0000_0000_0000 +page-size = 0o10000 +flag-bits = 0b1100_0011 +magic-number = 0xDEAD_BEEF + +[dependencies] +serde = { version = "1", features = ["derive"] } +tokio = { version = "1", features = ["full"], optional = true } +thiserror = "1" +tracing = "0.1" + +[dependencies.reqwest] +version = "0.12" +default-features = false +features = ["json", "rustls-tls"] +optional = true + +[dev-dependencies] +criterion = { version = "0.5", features = ["html_reports"] } +proptest = "1" +tempfile = "3" + +[[bench]] +name = "encode_throughput" +harness = false + +[[bench]] +name = "decode_throughput" +harness = false + +[[bench]] +name = "roundtrip" +harness = false + +[[example]] +name = "basic_usage" +required-features = ["std"] + +[[example]] +name = "async_client" +required-features = ["async", "serde-support"] + +[profile.release] +opt-level = 3 +lto = "thin" +codegen-units = 1 +strip = "symbols" +overflow-checks = false + +[profile.dev] +opt-level = 0 +debug = true +overflow-checks = true diff --git a/examples/data/ci-pipeline.toml b/examples/data/ci-pipeline.toml new file mode 100644 index 0000000..abc6406 --- /dev/null +++ b/examples/data/ci-pipeline.toml @@ -0,0 +1,71 @@ +[pipeline] +name = "my-app-ci" +version = 2 +timeout = 3_600 +fail-fast = true + +pipeline.meta.owner = "platform-team" +pipeline.meta.created = 2024-03-15 +pipeline.meta.description = """ +A multi-stage pipeline that builds, \ +tests, and deploys the application \ +to all target environments.""" + +[pipeline.triggers] +branches = ["main", "release/*", "hotfix/*"] +tags = ["v[0-9]*"] +on-pr = true + +[[pipeline.stages]] +name = "build" +image = "rust:1.78-slim" +allow-fail = false + +[pipeline.stages.env] +RUST_BACKTRACE = "1" +CARGO_TERM_COLOR = "always" + +[[pipeline.stages.steps]] +name = "restore-cache" +run = "cargo fetch --locked" + +[[pipeline.stages.steps]] +name = "compile" +run = "cargo build --release --locked" +timeout = 1_800 + +[[pipeline.stages]] +name = "test" +image = "rust:1.78-slim" +depends-on = ["build"] +allow-fail = false + +[pipeline.stages.env] +RUST_LOG = "debug" + +[[pipeline.stages.steps]] +name = "unit-tests" +run = "cargo test --workspace" + +[[pipeline.stages.steps]] +name = "coverage" +run = "cargo llvm-cov --lcov --output-path lcov.info" +timeout = 600 + +[[pipeline.stages]] +name = "deploy" +image = "alpine:3.19" +depends-on = ["build", "test"] +when = { branch = "main", event = "push" } + +[[pipeline.stages.steps]] +name = "upload-artifact" +run = "aws s3 cp ./target/release/my-app s3://releases/" + +[[pipeline.stages.steps]] +name = "notify-slack" +run = "curl -X POST $SLACK_WEBHOOK -d @payload.json" + +[pipeline.notifications] +on-success = { channel = "#deploys", message = "Deploy succeeded" } +on-failure = { channel = "#oncall", message = "Pipeline failed!" } diff --git a/examples/data/database-server.toml b/examples/data/database-server.toml new file mode 100644 index 0000000..13de59c --- /dev/null +++ b/examples/data/database-server.toml @@ -0,0 +1,147 @@ +cluster.name = "primary-pg-cluster" +cluster.version = "16.2" +cluster.region = "eu-central-1" + +"cluster.id" = "pg-eur-001" +"127.0.0.1" = "loopback-alias" +'special chars' = "spaces in key name" + +[audit] +cluster-created = 1970-01-01T00:00:00Z +last-failover = 2024-03-10T02:30:00-05:00 +last-maintenance = 2024-01-28T22:00:00+01:00 +last-config-edit = 2024-07-22T11:45:30 +last-vacuum-start = 2024-07-21T03:00:00.000 +certificate-expiry = 2025-08-14 +created-date = 2022-09-01 +daily-backup-time = 03:30:00 +maintenance-window = 22:00:00 +checkpoint-time = 00:00:00.000 + +[nodes.primary] +host = "pg-primary.internal" +port = 5432 +max-connections = 500 +shared-buffers = "4GB" + +nodes.primary.replication.slots = 4 +nodes.primary.replication.timeout = 60 +nodes.primary.replication.mode = "async" + +[nodes.primary.wal] +level = "replica" +archive = true +archive-dir = "/mnt/wal-archive/primary" +compression = "lz4" +keep-segments = 16 + +[nodes.primary.checkpoint] +completion-target = 0.9 +warning-seconds = 30 + +[[nodes.replicas]] +name = "replica-01" +host = "pg-replica-01.internal" +port = 5432 +priority = 100 +lag-warning-ms = 5_000 +lag-critical-ms = 30_000 +sync-mode = "async" +promoted = false + +[nodes.replicas.connection] +pool-size = 20 +keepalive = { idle = 60, interval = 10, count = 5 } + +[[nodes.replicas.slots]] +name = "wal_slot_app_01" +plugin = "pgoutput" +active = true + +[[nodes.replicas.slots]] +name = "wal_slot_analytics" +plugin = "wal2json" +active = false + +[[nodes.replicas]] +name = "replica-02" +host = "pg-replica-02.internal" +port = 5432 +priority = 90 +lag-warning-ms = 5_000 +lag-critical-ms = 30_000 +sync-mode = "sync" +promoted = false + +[nodes.replicas.connection] +pool-size = 15 +keepalive = { idle = 60, interval = 10, count = 5 } + +[[nodes.replicas.slots]] +name = "wal_slot_app_02" +plugin = "pgoutput" +active = true + +[storage] +block-size-decimal = 8192 +block-size-hex = 0x2000 +block-size-octal = 0o20000 +block-size-binary = 0b0010_0000_0000_0000 + +max-table-size-dec = 1_073_741_824 +max-table-size-hex = 0x4000_0000 + +file-permissions = 0o600 +cache-hit-target = 0.99 +bloat-threshold = 0.20 +fill-factor = 9.0e1 +compression-ratio = 3.5e+0 +dead-tuple-ratio = 1.0E-2 + +[monitoring] +scrape-interval = 15 +labels = { cluster = "primary-pg-cluster", env = "production" } + +alert-thresholds = [ + 0.8, + 0.9, + true, + "ops-team", +] + +retention-windows = [ + [60, "raw"], + [3600, "1h-rollup"], + [86400, "1d-rollup"], +] + +[[monitoring.alerts]] +name = "replication-lag" +severity = "critical" +threshold = 30_000 +enabled = true +created = 2023-11-01 + +[monitoring.alerts.notify] +channels = ["pagerduty", "slack-ops"] +cooldown = 300 + +[[monitoring.alerts.conditions]] +metric = "pg_replication_lag_bytes" +operator = ">" +value = 1_073_741_824 + +[[monitoring.alerts]] +name = "connection-saturation" +severity = "warning" +threshold = 0.85 +enabled = true + +[monitoring.alerts.notify] +channels = ["slack-ops"] +cooldown = 600 + +[[monitoring.alerts.conditions]] +metric = "pg_connections_ratio" +operator = ">=" +value = 0.85 diff --git a/src/functions/private/Add-TomlTableText.ps1 b/src/functions/private/Add-TomlTableText.ps1 index 412fa57..2b930a2 100644 --- a/src/functions/private/Add-TomlTableText.ps1 +++ b/src/functions/private/Add-TomlTableText.ps1 @@ -2,6 +2,22 @@ function Add-TomlTableText { <# .SYNOPSIS Appends TOML text for a table to a string builder. + + .DESCRIPTION + Recursively emits TOML-formatted assignments for all scalar and sub-table + keys in the given ordered dictionary. Sub-tables are emitted as [path] headers + after all scalars; arrays of tables are emitted last as [[path]] sections. + + .EXAMPLE + $sb = [System.Text.StringBuilder]::new() + Add-TomlTableText -StringBuilder $sb -Table $root -Path '' -EmitHeader:$false + Appends all root-level keys to $sb without a section header. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [void]. Content is appended to the StringBuilder passed via -StringBuilder. #> [CmdletBinding()] param( @@ -16,7 +32,7 @@ function Add-TomlTableText { [string] $Path, [Parameter(Mandatory)] - [bool] $EmitHeader + [switch] $EmitHeader ) if ($EmitHeader -and -not [string]::IsNullOrEmpty($Path)) { diff --git a/src/functions/private/ConvertFrom-TomlBasicStringValue.ps1 b/src/functions/private/ConvertFrom-TomlBasicStringValue.ps1 index 669e99d..ae35f13 100644 --- a/src/functions/private/ConvertFrom-TomlBasicStringValue.ps1 +++ b/src/functions/private/ConvertFrom-TomlBasicStringValue.ps1 @@ -2,6 +2,23 @@ function ConvertFrom-TomlBasicStringValue { <# .SYNOPSIS Parses a TOML basic string at the current source index. + + .DESCRIPTION + Reads a single-line basic string ("...") or a multi-line basic string ("""...""") + starting at $Index.Value in $Source. Processes TOML escape sequences (\n, \t, + \uXXXX, etc.) and advances $Index past the closing delimiter. + + .EXAMPLE + $src = '"hello\nworld"'; $i = 0 + ConvertFrom-TomlBasicStringValue -Source $src -Index ([ref]$i) + # Returns: "hello`nworld" + Parses a single-line basic string with an escape sequence. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [string] #> [OutputType([string])] [CmdletBinding()] diff --git a/src/functions/private/ConvertFrom-TomlDateTime.ps1 b/src/functions/private/ConvertFrom-TomlDateTime.ps1 index 2c4798d..1b66d94 100644 --- a/src/functions/private/ConvertFrom-TomlDateTime.ps1 +++ b/src/functions/private/ConvertFrom-TomlDateTime.ps1 @@ -6,6 +6,22 @@ function ConvertFrom-TomlDateTime { .DESCRIPTION Parses TOML local date, local time, local date-time, and offset date-time tokens and returns the corresponding PowerShell type. + + .EXAMPLE + ConvertFrom-TomlDateTime -Token '1979-05-27T07:32:00Z' + # Returns: [DateTimeOffset] 1979-05-27 07:32:00 +00:00 + Parses an offset date-time token. + + .EXAMPLE + ConvertFrom-TomlDateTime -Token '07:32:00' + # Returns: [TimeSpan] 07:32:00 + Parses a local time token. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [System.DateTimeOffset], [System.DateTime], or [System.TimeSpan] — depending on the token form. #> [OutputType([System.DateTimeOffset], [System.DateTime], [System.TimeSpan])] [CmdletBinding()] diff --git a/src/functions/private/ConvertFrom-TomlLiteralStringValue.ps1 b/src/functions/private/ConvertFrom-TomlLiteralStringValue.ps1 index 12a49fb..b51177e 100644 --- a/src/functions/private/ConvertFrom-TomlLiteralStringValue.ps1 +++ b/src/functions/private/ConvertFrom-TomlLiteralStringValue.ps1 @@ -2,6 +2,23 @@ function ConvertFrom-TomlLiteralStringValue { <# .SYNOPSIS Parses a TOML literal string at the current source index. + + .DESCRIPTION + Reads a single-line literal string ('...') or a multi-line literal string ('''...''') + starting at $Index.Value in $Source. No escape processing is performed. + Advances $Index past the closing delimiter. + + .EXAMPLE + $src = "'C:\Users\Alice'"; $i = 0 + ConvertFrom-TomlLiteralStringValue -Source $src -Index ([ref]$i) + # Returns: "C:\Users\Alice" + Parses a literal string containing backslashes with no escape processing. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [string] #> [OutputType([string])] [CmdletBinding()] diff --git a/src/functions/private/ConvertFrom-TomlParsedValue.ps1 b/src/functions/private/ConvertFrom-TomlParsedValue.ps1 index 952afa8..5ab978d 100644 --- a/src/functions/private/ConvertFrom-TomlParsedValue.ps1 +++ b/src/functions/private/ConvertFrom-TomlParsedValue.ps1 @@ -2,6 +2,30 @@ function ConvertFrom-TomlParsedValue { <# .SYNOPSIS Parses a TOML value at the current source index. + + .DESCRIPTION + Dispatches to the appropriate parser based on the leading character at + $Index.Value: basic string, literal string, inline array ([...]), inline + table ({...}), or a bare scalar token (boolean, integer, float, datetime). + Advances $Index past the consumed value. + + .EXAMPLE + $src = '"hello"'; $i = 0 + ConvertFrom-TomlParsedValue -Source $src -Index ([ref]$i) + # Returns: "hello" + Parses a basic string value. + + .EXAMPLE + $src = '42'; $i = 0 + ConvertFrom-TomlParsedValue -Source $src -Index ([ref]$i) + # Returns: 42 ([long]) + Parses an integer value. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [object] — type depends on the TOML value kind. #> [OutputType([object])] [CmdletBinding()] @@ -75,7 +99,7 @@ function ConvertFrom-TomlParsedValue { } elseif ($Source[$Index.Value] -eq '''') { ConvertFrom-TomlLiteralStringValue -Source $Source -Index $Index } else { - Get-TomlBareToken -Source $Source -Index $Index -StopAtEquals $true + Get-TomlBareToken -Source $Source -Index $Index -StopAtEquals } Skip-TomlWhitespace -Source $Source -Index $Index diff --git a/src/functions/private/ConvertFrom-TomlScalarToken.ps1 b/src/functions/private/ConvertFrom-TomlScalarToken.ps1 index fc7339a..9db48cf 100644 --- a/src/functions/private/ConvertFrom-TomlScalarToken.ps1 +++ b/src/functions/private/ConvertFrom-TomlScalarToken.ps1 @@ -2,6 +2,27 @@ function ConvertFrom-TomlScalarToken { <# .SYNOPSIS Converts a scalar TOML token to a PowerShell value. + + .DESCRIPTION + Interprets a bare scalar token string as the correct PowerShell type: + boolean, special float (inf/nan), datetime, hex/octal/binary/decimal + integer, or floating-point number. Throws for unrecognized tokens. + + .EXAMPLE + ConvertFrom-TomlScalarToken -Token 'true' + # Returns: [bool] $true + Converts the TOML boolean literal. + + .EXAMPLE + ConvertFrom-TomlScalarToken -Token '0xFF' + # Returns: 255 ([long]) + Converts a hexadecimal integer literal. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [object] — bool, double, long, DateTimeOffset, DateTime, or TimeSpan. #> [OutputType([object])] [CmdletBinding()] diff --git a/src/functions/private/ConvertFrom-TomlTable.ps1 b/src/functions/private/ConvertFrom-TomlTable.ps1 index 23bea8a..a43c3b4 100644 --- a/src/functions/private/ConvertFrom-TomlTable.ps1 +++ b/src/functions/private/ConvertFrom-TomlTable.ps1 @@ -2,6 +2,23 @@ function ConvertFrom-TomlTable { <# .SYNOPSIS Parses a TOML document string into an ordered dictionary. + + .DESCRIPTION + Processes TOML line-by-line: strips comments, handles standard table headers + ([...]), array-of-table headers ([[...]]), multi-line values (""", ''', [, {), + and dotted key assignments. Returns the root ordered dictionary preserving + key insertion order. Validates against duplicate keys and structural conflicts. + + .EXAMPLE + ConvertFrom-TomlTable -InputObject "title = `"Test`"`n[server]`nport = 80" + # Returns: OrderedDictionary { title = "Test", server = { port = 80 } } + Parses a minimal two-key TOML document. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [System.Collections.Specialized.OrderedDictionary] #> [OutputType([System.Collections.Specialized.OrderedDictionary])] [CmdletBinding()] diff --git a/src/functions/private/ConvertFrom-TomlValue.ps1 b/src/functions/private/ConvertFrom-TomlValue.ps1 index 7caeac4..e765c8f 100644 --- a/src/functions/private/ConvertFrom-TomlValue.ps1 +++ b/src/functions/private/ConvertFrom-TomlValue.ps1 @@ -2,6 +2,27 @@ function ConvertFrom-TomlValue { <# .SYNOPSIS Converts a TOML value token to a native PowerShell value. + + .DESCRIPTION + Entry point for full value parsing. Trims the token, delegates to + ConvertFrom-TomlParsedValue via a char-level index, and validates that + no trailing content remains after the value. + + .EXAMPLE + ConvertFrom-TomlValue -Value '"hello world"' + # Returns: "hello world" + Parses a basic string token. + + .EXAMPLE + ConvertFrom-TomlValue -Value '[1, 2, 3]' + # Returns: @(1, 2, 3) + Parses an inline array. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [object] — type depends on the TOML value. #> [OutputType([object])] [CmdletBinding()] diff --git a/src/functions/private/ConvertTo-TomlArrayObject.ps1 b/src/functions/private/ConvertTo-TomlArrayObject.ps1 index b9860da..da288bc 100644 --- a/src/functions/private/ConvertTo-TomlArrayObject.ps1 +++ b/src/functions/private/ConvertTo-TomlArrayObject.ps1 @@ -2,6 +2,22 @@ function ConvertTo-TomlArrayObject { <# .SYNOPSIS Normalizes a PowerShell enumerable to a TOML-compatible array. + + .DESCRIPTION + Iterates each item of the enumerable and passes it through + ConvertTo-TomlTableObject so that dictionaries, PSCustomObjects, and + nested arrays are each normalized recursively. Returns an object array. + + .EXAMPLE + ConvertTo-TomlArrayObject -Value @(1, 'two', $true) + # Returns: @(1, "two", $true) + Normalizes a mixed scalar array. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [object[]] #> [OutputType([object[]])] [CmdletBinding()] diff --git a/src/functions/private/ConvertTo-TomlTableObject.ps1 b/src/functions/private/ConvertTo-TomlTableObject.ps1 index a36531a..06ea9ab 100644 --- a/src/functions/private/ConvertTo-TomlTableObject.ps1 +++ b/src/functions/private/ConvertTo-TomlTableObject.ps1 @@ -2,6 +2,23 @@ function ConvertTo-TomlTableObject { <# .SYNOPSIS Normalizes input data to TOML-compatible objects. + + .DESCRIPTION + Converts any PowerShell value into a form the serializer can handle: + IDictionary → OrderedDictionary, PSCustomObject → OrderedDictionary, + IEnumerable (non-string) → object[] via ConvertTo-TomlArrayObject, + scalars → unchanged. Throws if the value is null. + + .EXAMPLE + ConvertTo-TomlTableObject -Value @{ a = 1; b = 'two' } + # Returns: [OrderedDictionary] { a = 1, b = "two" } + Converts a regular hashtable to an ordered dictionary. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [object] — OrderedDictionary, object[], or the original scalar. #> [OutputType([object])] [CmdletBinding()] diff --git a/src/functions/private/ConvertTo-TomlValue.ps1 b/src/functions/private/ConvertTo-TomlValue.ps1 index 2eb0578..eed39ec 100644 --- a/src/functions/private/ConvertTo-TomlValue.ps1 +++ b/src/functions/private/ConvertTo-TomlValue.ps1 @@ -2,6 +2,29 @@ function ConvertTo-TomlValue { <# .SYNOPSIS Converts a normalized value to a TOML literal. + + .DESCRIPTION + Serializes a single normalized PowerShell value to its TOML literal + representation: strings are quoted and escaped, booleans become true/false, + integers and floats use invariant-culture formatting, date/time types map to + their TOML forms, arrays become inline [ ... ], and dictionaries become + inline { ... }. Throws for null or unserializable types. + + .EXAMPLE + ConvertTo-TomlValue -Value 'hello"world' + # Returns: '"hello\"world"' + Serializes a string containing a double quote. + + .EXAMPLE + ConvertTo-TomlValue -Value ([System.DateTimeOffset]::Parse('1979-05-27T07:32:00Z')) + # Returns: '1979-05-27T07:32:00+00:00' + Serializes an offset date-time. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [string] #> [OutputType([string])] [CmdletBinding()] diff --git a/src/functions/private/Format-TomlKey.ps1 b/src/functions/private/Format-TomlKey.ps1 index 727368b..afacffb 100644 --- a/src/functions/private/Format-TomlKey.ps1 +++ b/src/functions/private/Format-TomlKey.ps1 @@ -2,6 +2,27 @@ function Format-TomlKey { <# .SYNOPSIS Formats a key for TOML output. + + .DESCRIPTION + Returns the key unchanged when it consists entirely of alphanumeric characters, + hyphens, and underscores (bare key). Otherwise wraps it in double quotes and + escapes internal backslashes and double quotes. + + .EXAMPLE + Format-TomlKey -Key 'server-host' + # Returns: 'server-host' + A bare key is returned as-is. + + .EXAMPLE + Format-TomlKey -Key 'my key' + # Returns: '"my key"' + A key with a space is wrapped in double quotes. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [string] #> [OutputType([string])] [CmdletBinding()] diff --git a/src/functions/private/Get-TomlBareToken.ps1 b/src/functions/private/Get-TomlBareToken.ps1 index 539d527..0f2fc6b 100644 --- a/src/functions/private/Get-TomlBareToken.ps1 +++ b/src/functions/private/Get-TomlBareToken.ps1 @@ -2,6 +2,24 @@ function Get-TomlBareToken { <# .SYNOPSIS Reads a bare TOML token from source. + + .DESCRIPTION + Advances $Index through $Source collecting characters until a delimiter + (comma, closing bracket, or closing brace) is encountered. When -StopAtEquals + is set, also stops at the equals sign, enabling bare key extraction in inline + tables. Returns the trimmed token. + + .EXAMPLE + $src = 'true, 42'; $i = 0 + Get-TomlBareToken -Source $src -Index ([ref]$i) + # Returns: 'true' + Reads a bare token up to the comma delimiter. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [string] #> [OutputType([string])] [CmdletBinding()] @@ -13,7 +31,7 @@ function Get-TomlBareToken { [ref] $Index, [Parameter()] - [bool] $StopAtEquals = $false + [switch] $StopAtEquals ) $start = $Index.Value diff --git a/src/functions/private/Get-TomlContentWithoutComment.ps1 b/src/functions/private/Get-TomlContentWithoutComment.ps1 index 20e7b30..5af75ff 100644 --- a/src/functions/private/Get-TomlContentWithoutComment.ps1 +++ b/src/functions/private/Get-TomlContentWithoutComment.ps1 @@ -2,6 +2,22 @@ function Get-TomlContentWithoutComment { <# .SYNOPSIS Removes TOML inline comments while preserving quoted text. + + .DESCRIPTION + Scans a TOML source line character-by-character, tracking basic-string and + literal-string context to avoid treating # inside a string as a comment + delimiter. Returns the content up to (but not including) the first unquoted #. + + .EXAMPLE + Get-TomlContentWithoutComment -Line 'port = 5432 # the DB port' + # Returns: 'port = 5432 ' + Strips the inline comment from a key-value line. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [string] #> [OutputType([string])] [CmdletBinding()] diff --git a/src/functions/private/Get-TomlNestedTable.ps1 b/src/functions/private/Get-TomlNestedTable.ps1 index 78c5e30..a630ff7 100644 --- a/src/functions/private/Get-TomlNestedTable.ps1 +++ b/src/functions/private/Get-TomlNestedTable.ps1 @@ -2,6 +2,25 @@ function Get-TomlNestedTable { <# .SYNOPSIS Resolves or creates nested table path segments. + + .DESCRIPTION + Walks the path segments into the root ordered dictionary, creating + intermediate tables when absent. When a segment resolves to an + ArrayList (array-of-tables), the last entry of the array is used + as the current table context, matching TOML's semantics for nested + table headers inside [[arr]] blocks. + + .EXAMPLE + $root = [ordered]@{} + Get-TomlNestedTable -StartTable $root -Segments @('a', 'b') -ArrayLastTableByPath @{} + # Returns: the OrderedDictionary at root['a']['b'], creating it if needed. + Resolves a two-level path, creating intermediate tables. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [System.Collections.Specialized.OrderedDictionary] #> [OutputType([System.Collections.Specialized.OrderedDictionary])] [CmdletBinding()] diff --git a/src/functions/private/Join-TomlKeyPath.ps1 b/src/functions/private/Join-TomlKeyPath.ps1 index bf1d982..5154f46 100644 --- a/src/functions/private/Join-TomlKeyPath.ps1 +++ b/src/functions/private/Join-TomlKeyPath.ps1 @@ -2,6 +2,22 @@ function Join-TomlKeyPath { <# .SYNOPSIS Joins key path segments into a dotted path. + + .DESCRIPTION + Concatenates an array of key segments with a dot separator. Returns an + empty string for a null or empty segment array. Used to build the canonical + path string for header-deduplication tracking. + + .EXAMPLE + Join-TomlKeyPath -Segments @('a', 'b', 'c') + # Returns: 'a.b.c' + Joins three segments into a dotted path. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [string] #> [OutputType([string])] [CmdletBinding()] diff --git a/src/functions/private/Skip-TomlWhitespace.ps1 b/src/functions/private/Skip-TomlWhitespace.ps1 index 0d681e1..cf3a352 100644 --- a/src/functions/private/Skip-TomlWhitespace.ps1 +++ b/src/functions/private/Skip-TomlWhitespace.ps1 @@ -2,7 +2,25 @@ function Skip-TomlWhitespace { <# .SYNOPSIS Advances an index past whitespace in a TOML source string. + + .DESCRIPTION + Increments $Index.Value while the character at that position in $Source is + classified as whitespace by [char]::IsWhiteSpace. Used to position the index + before the next meaningful character during value parsing. + + .EXAMPLE + $src = ' 42'; $i = 0 + Skip-TomlWhitespace -Source $src -Index ([ref]$i) + # $i is now 3, pointing at '4' + Skips three leading spaces. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [void] #> + [OutputType([void])] [CmdletBinding()] param( [Parameter(Mandatory)] diff --git a/src/functions/private/Split-TomlDottedKey.ps1 b/src/functions/private/Split-TomlDottedKey.ps1 index 3a6d49e..68a5cb8 100644 --- a/src/functions/private/Split-TomlDottedKey.ps1 +++ b/src/functions/private/Split-TomlDottedKey.ps1 @@ -2,6 +2,28 @@ function Split-TomlDottedKey { <# .SYNOPSIS Splits a TOML dotted key into normalized key segments. + + .DESCRIPTION + Tokenizes a TOML key path respecting basic-string ("...") and literal-string + ('...') quoting, so dots inside quoted segments are not treated as separators. + Quoted segments are decoded (basic) or used as-is (literal). Bare segments are + returned unchanged. Throws for empty segments. + + .EXAMPLE + Split-TomlDottedKey -KeyPath 'a.b.c' + # Returns: @('a', 'b', 'c') + Splits a simple dotted key. + + .EXAMPLE + Split-TomlDottedKey -KeyPath '"my.key".sub' + # Returns: @('my.key', 'sub') + Dot inside a quoted segment is treated as literal. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [string[]] #> [OutputType([string[]])] [CmdletBinding()] diff --git a/src/functions/private/Test-TomlEndsWithDoubleNewLine.ps1 b/src/functions/private/Test-TomlEndsWithDoubleNewLine.ps1 index b6c5fb6..1de98de 100644 --- a/src/functions/private/Test-TomlEndsWithDoubleNewLine.ps1 +++ b/src/functions/private/Test-TomlEndsWithDoubleNewLine.ps1 @@ -2,6 +2,24 @@ function Test-TomlEndsWithDoubleNewLine { <# .SYNOPSIS Tests whether a StringBuilder ends with two LF characters. + + .DESCRIPTION + Inspects the last two characters of the given StringBuilder to determine + whether the buffer already ends with a blank line (two consecutive LF + characters). Used by the serializer to avoid inserting extra blank lines + between sections. + + .EXAMPLE + $sb = [System.Text.StringBuilder]::new("line1`n`n") + Test-TomlEndsWithDoubleNewLine -StringBuilder $sb + # Returns: $true + Detects a trailing blank line. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [bool] #> [OutputType([bool])] [CmdletBinding()] diff --git a/src/functions/private/Test-TomlTableArray.ps1 b/src/functions/private/Test-TomlTableArray.ps1 index e2c0c1f..164ddc5 100644 --- a/src/functions/private/Test-TomlTableArray.ps1 +++ b/src/functions/private/Test-TomlTableArray.ps1 @@ -2,6 +2,31 @@ function Test-TomlTableArray { <# .SYNOPSIS Tests whether a value is an array of TOML tables. + + .DESCRIPTION + Returns $true when the value is a non-empty IEnumerable (but not a string + or IDictionary) whose every element is an OrderedDictionary. This is used by + the serializer to distinguish TOML arrays-of-tables from regular scalar arrays. + + .EXAMPLE + $aot = [System.Collections.ArrayList]@( + [ordered]@{ name = 'a' }, + [ordered]@{ name = 'b' } + ) + Test-TomlTableArray -Value $aot + # Returns: $true + A list of ordered dictionaries is an array of tables. + + .EXAMPLE + Test-TomlTableArray -Value @(1, 2, 3) + # Returns: $false + A scalar array is not an array of tables. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [bool] #> [OutputType([bool])] [CmdletBinding()] diff --git a/src/functions/public/ConvertFrom-Toml.ps1 b/src/functions/public/ConvertFrom-Toml.ps1 index 097d7e7..0f211da 100644 --- a/src/functions/public/ConvertFrom-Toml.ps1 +++ b/src/functions/public/ConvertFrom-Toml.ps1 @@ -7,6 +7,43 @@ function ConvertFrom-Toml { Parses TOML-formatted text into a TomlDocument object with OrderedDictionary semantics and TOML-compatible scalar mappings. + Supported scalar types and their PowerShell equivalents: + - String → [string] + - Integer → [long] + - Float → [double] + - Boolean → [bool] + - Offset date-time → [System.DateTimeOffset] + - Local date-time → [System.DateTime] (Kind = Unspecified) + - Local date → [System.DateTime] (time = 00:00:00) + - Local time → [System.TimeSpan] + - Array → [object[]] + - Table → [System.Collections.Specialized.OrderedDictionary] + + .EXAMPLE + $doc = ConvertFrom-Toml -InputObject @' + [server] + host = "localhost" + port = 8080 + '@ + $doc.Data.server.host # "localhost" + + Parses an inline TOML string and accesses a nested value. + + .EXAMPLE + 'title = "My Doc"' | ConvertFrom-Toml + + Converts a TOML string from the pipeline. + + .INPUTS + [string] + + .OUTPUTS + [TomlDocument] + + .NOTES + Throws [System.InvalidOperationException] for any TOML syntax error, + duplicate key, or structural violation. + .LINK https://psmodule.io/Toml/Functions/ConvertFrom-Toml #> @@ -19,6 +56,7 @@ function ConvertFrom-Toml { ) process { + Write-Verbose "Parsing TOML string ($($InputObject.Length) character(s))." try { $data = ConvertFrom-TomlTable -InputObject $InputObject return [TomlDocument]::new($data) diff --git a/src/functions/public/ConvertTo-Toml.ps1 b/src/functions/public/ConvertTo-Toml.ps1 index 886ac75..a4b3970 100644 --- a/src/functions/public/ConvertTo-Toml.ps1 +++ b/src/functions/public/ConvertTo-Toml.ps1 @@ -3,6 +3,48 @@ function ConvertTo-Toml { .SYNOPSIS Converts a PowerShell object graph to TOML text. + .DESCRIPTION + Serializes a PowerShell object — TomlDocument, hashtable, ordered dictionary, + or PSCustomObject — into a TOML string. Nested dictionaries become TOML tables; + arrays of dictionaries become TOML arrays of tables. + + Supported PowerShell → TOML type mappings: + - [string] → basic string (special characters escaped) + - [bool] → true / false + - Integer types → TOML integer + - [double] / [float] → TOML float (inf, -inf, nan handled) + - [System.DateTimeOffset] → offset date-time + - [System.DateTime] → local date or local date-time + - [System.TimeSpan] → local time + - Scalar [object[]] → inline TOML array + - IDictionary → TOML table + - Array of IDictionary → TOML array of tables + + .EXAMPLE + $toml = ConvertTo-Toml -InputObject ([ordered]@{ + title = 'My App' + server = [ordered]@{ host = 'localhost'; port = 8080 } + }) + Write-Host $toml + + Serializes a nested ordered dictionary to TOML text. + + .EXAMPLE + Import-Toml -Path 'config.toml' | ConvertTo-Toml + + Round-trips a TOML file back to TOML text. + + .INPUTS + [object] — pipeline input supported. + + .OUTPUTS + [string] + + .NOTES + Throws when the object graph contains null values (TOML has no null type), + or types that cannot be serialized such as script blocks or COM objects. + Key order is preserved when the input uses an ordered dictionary. + .LINK https://psmodule.io/Toml/Functions/ConvertTo-Toml #> @@ -15,6 +57,7 @@ function ConvertTo-Toml { ) process { + Write-Verbose "Serializing object graph to TOML." $source = if ($InputObject -is [TomlDocument]) { $InputObject.Data } else { diff --git a/src/functions/public/Export-Toml.ps1 b/src/functions/public/Export-Toml.ps1 index 4f338fd..0fba427 100644 --- a/src/functions/public/Export-Toml.ps1 +++ b/src/functions/public/Export-Toml.ps1 @@ -55,6 +55,7 @@ ) process { + Write-Verbose "Serializing object graph to TOML." $tomlString = ConvertTo-Toml -InputObject $InputObject $resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) @@ -65,6 +66,7 @@ } if ($PSCmdlet.ShouldProcess($resolvedPath, 'Write TOML file')) { + Write-Verbose "Writing $($tomlString.Length) character(s) to: $resolvedPath" [System.IO.File]::WriteAllText( $resolvedPath, $tomlString, diff --git a/src/functions/public/Import-Toml.ps1 b/src/functions/public/Import-Toml.ps1 index f92db74..a1581f2 100644 --- a/src/functions/public/Import-Toml.ps1 +++ b/src/functions/public/Import-Toml.ps1 @@ -48,8 +48,10 @@ process { $resolvedPath = Resolve-Path -Path $Path -ErrorAction Stop + Write-Verbose "Importing TOML file: $($resolvedPath.ProviderPath)" $content = [System.IO.File]::ReadAllText($resolvedPath.ProviderPath) + Write-Verbose "Read $($content.Length) character(s). Parsing..." $doc = ConvertFrom-Toml -InputObject $content $doc.FilePath = $resolvedPath.ProviderPath diff --git a/tests/BeforeAll.ps1 b/tests/BeforeAll.ps1 new file mode 100644 index 0000000..1981541 --- /dev/null +++ b/tests/BeforeAll.ps1 @@ -0,0 +1,3 @@ +# Shared setup executed once before the test matrix. +# The module is imported by the CI runner before tests are invoked. +# Add any session-wide test fixtures or environment configuration here. From ab486731294af80ee47a86c879259d161800e3a2 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 26 Jul 2026 11:20:18 +0200 Subject: [PATCH 10/13] =?UTF-8?q?=F0=9F=A9=B9=20[Patch]:=20Add=20Test-Toml?= =?UTF-8?q?=20to=20validate=20TOML=20without=20throwing=20(#22)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `Test-Toml`, a pure-validator that returns `[bool]` — never throws — so scripts can safely probe TOML content before (or instead of) parsing it. The design mirrors PowerShell's built-in `Test-Json`. ## New: Validate TOML content safely `Test-Toml` wraps `ConvertFrom-Toml` in a try/catch. On success it returns `$true`; on any parser error it writes a non-terminating error to the error stream and returns `$false`. ```powershell # Validate a string Test-Toml -InputObject 'key = "value"' # $true '[invalid' | Test-Toml # $false + error written # Validate a file Test-Toml -Path .\Cargo.toml # $true if file is valid TOML Test-Toml -LiteralPath 'C:\app\cfg.toml' # literal path variant ``` **Parameter sets** | Parameter | Set | Notes | |---|---|---| | `-InputObject` | Default | Pipeline-capable; `[AllowEmptyString()]` so `''` returns `$false` | | `-Path` | `Path` | Resolves relative paths | | `-LiteralPath` | `LiteralPath` | No wildcard expansion | **Key implementation notes** - `[AllowEmptyString()]` is required on `InputObject`: PowerShell's mandatory parameter binding rejects `''` without it, whereas the correct behaviour is `$false`. - `Write-Error` is always called from inside a `catch` block with `-ErrorAction Continue` to ensure non-terminating behaviour even when `$ErrorActionPreference = 'Stop'` is set module-wide. - No new parser logic — reuses `ConvertFrom-Toml` to keep validation consistent with parsing. ## Technical Details - **File added:** `src/functions/public/Test-Toml.ps1` - **Tests added:** `Describe 'Test-Toml'` block in `tests/Toml.Tests.ps1` (11 new tests; total 135, all passing) - **PSScriptAnalyzer:** No new warnings introduced (pre-existing BOM and OutputType notices in other files unchanged) - **No alias:** The issue mentions `Test-Tml` but aliases are not in the current module standards

Closes #4
--------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .gitignore | 3 + src/functions/public/Test-Toml.ps1 | 119 +++++++++++++++++++++++++++++ tests/Toml.Tests.ps1 | 104 +++++++++++++++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 src/functions/public/Test-Toml.ps1 diff --git a/.gitignore b/.gitignore index 4238184..fe7a40e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ output/ bin/ obj/ libs/ + +# Pester test output +testResults.xml diff --git a/src/functions/public/Test-Toml.ps1 b/src/functions/public/Test-Toml.ps1 new file mode 100644 index 0000000..a06bc93 --- /dev/null +++ b/src/functions/public/Test-Toml.ps1 @@ -0,0 +1,119 @@ +function Test-Toml { + <# + .SYNOPSIS + Tests whether a string or file contains valid TOML. + + .DESCRIPTION + Validates TOML content without throwing. Returns $true when the input + parses successfully and $false when it does not. On failure, a + non-terminating error is written via Write-Error so the caller can + inspect $Error or use -ErrorVariable while the pipeline continues. + + Three parameter sets are supported: + - Default : -InputObject — validates a TOML string directly. + - Path : -Path — reads a file (relative paths resolved). + - LiteralPath: -LiteralPath — reads a file (no wildcard expansion). + + .EXAMPLE + Test-Toml -InputObject 'title = "Hello"' + + Returns $true because the string is valid TOML. + + .EXAMPLE + Test-Toml -InputObject 'bad = = "syntax"' + + Returns $false and writes a non-terminating error describing the parse + failure. + + .EXAMPLE + Test-Toml -Path '.\config.toml' + + Reads the file and returns $true if its content is valid TOML. + + .EXAMPLE + Test-Toml -LiteralPath 'C:\Configs\app.toml' + + Reads the file using a literal path (no glob expansion) and returns + $true if the content parses successfully. + + .INPUTS + [string] — pipeline input supported for the InputObject parameter. + + .OUTPUTS + [bool] + + .NOTES + Mirrors the pattern of the built-in Test-Json cmdlet. + All exceptions from the parser are caught and surfaced as + non-terminating errors so the function never throws. + + .LINK + https://psmodule.io/Toml/Functions/Test-Toml + #> + [OutputType([bool])] + [CmdletBinding(DefaultParameterSetName = 'Default')] + param( + # The TOML string to validate. Accepts pipeline input. + [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Default')] + [AllowEmptyString()] + [string] $InputObject, + + # Path to a TOML file to validate. Relative paths are resolved against + # the current working directory. + [Parameter(Mandatory, ParameterSetName = 'Path')] + [string] $Path, + + # Literal path to a TOML file to validate. No wildcard expansion is + # performed. + [Parameter(Mandatory, ParameterSetName = 'LiteralPath')] + [string] $LiteralPath + ) + + process { + $content = $null + + if ($PSCmdlet.ParameterSetName -eq 'Path') { + Write-Verbose "Resolving path: $Path" + try { + $resolved = Resolve-Path -Path $Path -ErrorAction Stop + } catch { + Write-Error -Message "Cannot find path '$Path': $($_.Exception.Message)" -ErrorAction Continue + return $false + } + try { + $content = [System.IO.File]::ReadAllText($resolved.ProviderPath) + } catch { + Write-Error -Message "Cannot read file '$($resolved.ProviderPath)': $($_.Exception.Message)" -ErrorAction Continue + return $false + } + } elseif ($PSCmdlet.ParameterSetName -eq 'LiteralPath') { + Write-Verbose "Using literal path: $LiteralPath" + try { + $resolved = Resolve-Path -LiteralPath $LiteralPath -ErrorAction Stop + } catch { + Write-Error -Message "Cannot find path '$LiteralPath': $($_.Exception.Message)" -ErrorAction Continue + return $false + } + try { + $content = [System.IO.File]::ReadAllText($resolved.ProviderPath) + } catch { + Write-Error -Message "Cannot read file '$($resolved.ProviderPath)': $($_.Exception.Message)" -ErrorAction Continue + return $false + } + } else { + $content = $InputObject + } + + Write-Verbose "Validating TOML content ($($content.Length) character(s))." + try { + if ([string]::IsNullOrEmpty($content)) { + throw [System.ArgumentException]::new('Input is empty.') + } + $null = ConvertFrom-Toml -InputObject $content + return $true + } catch { + Write-Error -Message "TOML validation failed: $($_.Exception.Message)" -ErrorAction Continue + return $false + } + } +} diff --git a/tests/Toml.Tests.ps1 b/tests/Toml.Tests.ps1 index 1cbbcc3..37954f3 100644 --- a/tests/Toml.Tests.ps1 +++ b/tests/Toml.Tests.ps1 @@ -15,6 +15,7 @@ Describe 'Toml' { $commands | Should -Contain 'ConvertTo-Toml' $commands | Should -Contain 'Import-Toml' $commands | Should -Contain 'Export-Toml' + $commands | Should -Contain 'Test-Toml' } } @@ -801,4 +802,107 @@ Describe 'Toml' { } } } + + Describe 'Test-Toml' { + + Context 'Module registration' { + It 'is exported from the module' { + Get-Command -Module Toml | Select-Object -ExpandProperty Name | Should -Contain 'Test-Toml' + } + + It 'declares [OutputType([bool])]' { + $cmd = Get-Command -Name Test-Toml + $cmd.OutputType.Type | Should -Contain ([bool]) + } + } + + Context 'InputObject — valid TOML' { + It 'returns $true for a minimal key=value string' { + $result = Test-Toml -InputObject 'key = "value"' + $result | Should -BeTrue + } + + It 'returns a [bool]' { + $result = Test-Toml -InputObject 'key = 1' + $result | Should -BeOfType [bool] + } + + It 'accepts pipeline input and returns $true' { + $result = 'enabled = true' | Test-Toml + $result | Should -BeTrue + } + + It 'returns $true for a multi-section TOML string' { + $toml = @' +[server] +host = "localhost" +port = 8080 +'@ + Test-Toml -InputObject $toml | Should -BeTrue + } + } + + Context 'InputObject — invalid TOML' { + It 'returns $false for invalid syntax' { + $result = Test-Toml -InputObject 'bad = = "syntax"' 2>$null + $result | Should -BeFalse + } + + It 'writes a non-terminating error for invalid TOML' { + $errors = @() + $result = Test-Toml -InputObject 'bad = = "syntax"' -ErrorVariable errors 2>$null + $result | Should -BeFalse + $errors | Should -Not -BeNullOrEmpty + } + + It 'returns $false for an empty string' { + $result = Test-Toml -InputObject '' 2>$null + $result | Should -BeFalse + } + + It 'returns $false for a duplicate-key document' { + $result = Test-Toml -InputObject "key = 1`nkey = 2" 2>$null + $result | Should -BeFalse + } + } + + Context 'Path — valid file' { + It 'returns $true for a valid TOML file via -Path' { + $path = Join-Path $TestDrive 'valid.toml' + Set-Content -Path $path -Value 'name = "test"' -Encoding UTF8 + Test-Toml -Path $path | Should -BeTrue + } + } + + Context 'Path — invalid file' { + It 'returns $false for an invalid TOML file via -Path' { + $path = Join-Path $TestDrive 'invalid.toml' + Set-Content -Path $path -Value 'bad = = "syntax"' -Encoding UTF8 + $result = Test-Toml -Path $path 2>$null + $result | Should -BeFalse + } + + It 'returns $false and writes an error for a non-existent file via -Path' { + $errors = @() + $result = Test-Toml -Path (Join-Path $TestDrive 'does-not-exist.toml') -ErrorVariable errors 2>$null + $result | Should -BeFalse + $errors | Should -Not -BeNullOrEmpty + } + } + + Context 'LiteralPath — valid file' { + It 'returns $true for a valid TOML file via -LiteralPath' { + $path = Join-Path $TestDrive 'literal-valid.toml' + Set-Content -Path $path -Value 'answer = 42' -Encoding UTF8 + Test-Toml -LiteralPath $path | Should -BeTrue + } + + It 'returns a [bool] via -LiteralPath' { + $path = Join-Path $TestDrive 'literal-bool.toml' + Set-Content -Path $path -Value 'flag = true' -Encoding UTF8 + $result = Test-Toml -LiteralPath $path + $result | Should -BeOfType [bool] + } + } + } } From 2ea94553b28317a3a636b2852c8b1ffe80e70a9f Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 26 Jul 2026 11:28:25 +0200 Subject: [PATCH 11/13] =?UTF-8?q?=F0=9F=9A=80=20[Feature]:=20Merge-Toml=20?= =?UTF-8?q?combines=20TOML=20documents=20into=20one=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Merge-Toml` combines two or more TOML documents into one, so layered configuration — defaults plus environment overrides, or a base file plus a local override file — no longer needs ad-hoc hashtable-merging loops. ## New: Merge-Toml `Merge-Toml` accepts either two TOML strings, or a list of file paths merged in order: ```powershell Merge-Toml -BaseObject $defaults -OverrideObject $overrides Merge-Toml -Path 'defaults.toml', 'local.toml' Merge-Toml -LiteralPath 'defaults.toml', 'local.toml' ``` Nested tables are always deep-merged recursively, and arrays of tables (`[[...]]`) are always concatenated, base entries first. Scalar key conflicts (including inline arrays, treated as scalars) are resolved with `-Strategy`: - `LastWins` (default) — the override value replaces the base value - `FirstWins` — the base value is kept - `ErrorOnConflict` — throws on any duplicate scalar key The result is always a `[string]` of canonical TOML, parseable by `ConvertFrom-Toml`. ---
Technical details - Added `src/functions/public/Merge-Toml.ps1` with three parameter sets: `Default` (`-BaseObject`/`-OverrideObject`), `Path`, and `LiteralPath`. - Added private recursive helper `src/functions/private/Merge-TomlTableObject.ps1` that walks override keys against a base `OrderedDictionary`, deep-merging nested tables, concatenating `ArrayList` array-of-tables, and applying `-Strategy` to remaining scalar conflicts. - Implementation reuses `ConvertFrom-Toml`/`ConvertTo-Toml` for parsing and serialization — no new parsing logic. - Added Pester coverage in `tests/Toml.Tests.ps1` for all parameter sets, all three strategies, deep-merge, AoT concatenation, file input, and idempotency. - Updated `README.md` with a usage example and added `Merge-Toml` to the command table. - Verified: `Invoke-ScriptAnalyzer -Path src/ -Recurse` clean (no new findings), 135/135 Pester tests passing. - Implementation plan progress: all tasks in #19 completed (helper, function, tests, README).
Relevant issues (or links) - Closes PSModule/Toml#19
--------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 23 +++ .../private/Merge-TomlTableObject.ps1 | 101 +++++++++++++ src/functions/public/Merge-Toml.ps1 | 111 ++++++++++++++ tests/Toml.Tests.ps1 | 143 ++++++++++++++++++ 4 files changed, 378 insertions(+) create mode 100644 src/functions/private/Merge-TomlTableObject.ps1 create mode 100644 src/functions/public/Merge-Toml.ps1 diff --git a/README.md b/README.md index 967fed8..8910eea 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,28 @@ $doc.Data['version'] = 2 Export-Toml -InputObject $doc -Path './config.toml' ``` +### Merge two TOML documents + +```powershell +$defaults = @' +[server] +host = "localhost" +port = 8080 +'@ + +$overrides = @' +[server] +port = 9090 +'@ + +Merge-Toml -BaseObject $defaults -OverrideObject $overrides +# [server] +# host = "localhost" +# port = 9090 +``` + +`Merge-Toml` also accepts `-Path`/`-LiteralPath` for merging files, and a `-Strategy` of `LastWins` (default), `FirstWins`, or `ErrorOnConflict` for resolving scalar key conflicts. Nested tables are always deep-merged and arrays of tables are always concatenated. + ## TOML type mapping | TOML type | PowerShell type | @@ -102,6 +124,7 @@ Export-Toml -InputObject $doc -Path './config.toml' | `ConvertTo-Toml` | Serialize object → TOML text | | `Import-Toml` | Read TOML file → `TomlDocument` | | `Export-Toml` | Write object or `TomlDocument` to file | +| `Merge-Toml` | Merge two TOML documents into one | ## Implementation notes diff --git a/src/functions/private/Merge-TomlTableObject.ps1 b/src/functions/private/Merge-TomlTableObject.ps1 new file mode 100644 index 0000000..1629974 --- /dev/null +++ b/src/functions/private/Merge-TomlTableObject.ps1 @@ -0,0 +1,101 @@ +function Merge-TomlTableObject { + <# + .SYNOPSIS + Recursively merges two TOML table dictionaries. + + .DESCRIPTION + Walks every key in the override dictionary and combines it with the base + dictionary. Keys missing from the base are added as-is. Nested tables + (OrderedDictionary) are merged recursively. Arrays of tables (ArrayList of + OrderedDictionary) are concatenated, base entries first. Any other + overlapping key — scalar or inline array — is resolved with the given + merge strategy: LastWins keeps the override value, FirstWins keeps the + base value, and ErrorOnConflict throws. + + .EXAMPLE + $base = [ordered]@{ a = 1; server = [ordered]@{ host = 'localhost' } } + $override = [ordered]@{ b = 2; server = [ordered]@{ port = 80 } } + Merge-TomlTableObject -Base $base -Override $override -Strategy 'LastWins' + # Returns: OrderedDictionary { a = 1, server = { host = 'localhost', port = 80 }, b = 2 } + + Deep-merges a nested table while preserving keys unique to each side. + + .EXAMPLE + $base = [ordered]@{ key = 'base' } + $override = [ordered]@{ key = 'override' } + Merge-TomlTableObject -Base $base -Override $override -Strategy 'ErrorOnConflict' + # Throws because 'key' is defined on both sides. + + Demonstrates conflict detection for duplicate scalar keys. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [System.Collections.Specialized.OrderedDictionary] + #> + [OutputType([System.Collections.Specialized.OrderedDictionary])] + [CmdletBinding()] + param( + # The base table. Its keys are preserved unless overridden. + [Parameter(Mandatory)] + [System.Collections.Specialized.OrderedDictionary] $Base, + + # The override table applied on top of the base. + [Parameter(Mandatory)] + [System.Collections.Specialized.OrderedDictionary] $Override, + + # The strategy used to resolve scalar key conflicts. + [Parameter(Mandatory)] + [ValidateSet('LastWins', 'FirstWins', 'ErrorOnConflict')] + [string] $Strategy + ) + + $result = [System.Collections.Specialized.OrderedDictionary]::new([System.StringComparer]::Ordinal) + foreach ($key in $Base.Keys) { + $result[$key] = $Base[$key] + } + + foreach ($key in $Override.Keys) { + if (-not $result.Contains($key)) { + Write-Verbose "Adding override-only key '$key'." + $result[$key] = $Override[$key] + continue + } + + $baseValue = $result[$key] + $overrideValue = $Override[$key] + + if ($baseValue -is [System.Collections.Specialized.OrderedDictionary] -and + $overrideValue -is [System.Collections.Specialized.OrderedDictionary]) { + Write-Verbose "Deep-merging nested table for key '$key'." + $result[$key] = Merge-TomlTableObject -Base $baseValue -Override $overrideValue -Strategy $Strategy + continue + } + + if ($baseValue -is [System.Collections.ArrayList] -and $overrideValue -is [System.Collections.ArrayList]) { + Write-Verbose "Concatenating array-of-tables for key '$key'." + $combined = [System.Collections.ArrayList]::new($baseValue) + $null = $combined.AddRange($overrideValue) + $result[$key] = $combined + continue + } + + switch ($Strategy) { + 'LastWins' { + Write-Verbose "Resolving scalar conflict for key '$key' using LastWins." + $result[$key] = $overrideValue + } + 'FirstWins' { + Write-Verbose "Resolving scalar conflict for key '$key' using FirstWins." + } + 'ErrorOnConflict' { + throw [System.InvalidOperationException]::new( + "The key '$key' is defined in both documents and the merge strategy is 'ErrorOnConflict'." + ) + } + } + } + + return $result +} diff --git a/src/functions/public/Merge-Toml.ps1 b/src/functions/public/Merge-Toml.ps1 new file mode 100644 index 0000000..1818dd7 --- /dev/null +++ b/src/functions/public/Merge-Toml.ps1 @@ -0,0 +1,111 @@ +function Merge-Toml { + <# + .SYNOPSIS + Merges two or more TOML documents into one. + + .DESCRIPTION + Parses TOML documents with ConvertFrom-Toml, deep-merges the resulting + ordered dictionaries with the private Merge-TomlTableObject helper, and + serializes the combined result back to TOML text with ConvertTo-Toml. + + Nested tables are always merged recursively. Arrays of tables are always + concatenated, base entries first, then override entries. Scalar key + conflicts (including inline arrays, which are treated as scalars) are + resolved with -Strategy: + - LastWins (default): the override value replaces the base value + - FirstWins: the base value is kept and the override value is ignored + - ErrorOnConflict: a duplicate scalar key throws + + When -Path or -LiteralPath is given with more than two files, documents + are merged left to right: the first file is the base, and each + subsequent file is merged on top of the accumulated result in order. + + .EXAMPLE + Merge-Toml -BaseObject 'a = 1' -OverrideObject 'b = 2' + # Returns: "a = 1`nb = 2" + + Merges two TOML strings with no overlapping keys. + + .EXAMPLE + Merge-Toml -Path 'defaults.toml', 'local.toml' -Strategy 'FirstWins' + + Merges two files in order, keeping the base value whenever both files + define the same scalar key. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [string] + + .NOTES + Throws when a file cannot be found or read, when either document is not + valid TOML, or when -Strategy 'ErrorOnConflict' encounters a duplicate + scalar key. + + .LINK + https://psmodule.io/Toml/Functions/Merge-Toml + #> + [OutputType([string])] + [CmdletBinding(DefaultParameterSetName = 'Default')] + param( + # The base TOML document, provided as a string. + [Parameter(Mandatory, ParameterSetName = 'Default')] + [ValidateNotNullOrEmpty()] + [string] $BaseObject, + + # The override TOML document applied on top of the base document. + [Parameter(Mandatory, ParameterSetName = 'Default')] + [ValidateNotNullOrEmpty()] + [string] $OverrideObject, + + # File paths to merge in order. The first path is the base document, + # and each subsequent path is merged on top of the accumulated result. + [Parameter(Mandatory, ParameterSetName = 'Path')] + [ValidateCount(2, [int]::MaxValue)] + [string[]] $Path, + + # Literal file paths to merge in order, without wildcard expansion. + # The first path is the base document, and each subsequent path is + # merged on top of the accumulated result. + [Parameter(Mandatory, ParameterSetName = 'LiteralPath')] + [ValidateCount(2, [int]::MaxValue)] + [string[]] $LiteralPath, + + # The strategy used to resolve scalar key conflicts between documents. + [Parameter()] + [ValidateSet('LastWins', 'FirstWins', 'ErrorOnConflict')] + [string] $Strategy = 'LastWins' + ) + + process { + $documents = switch ($PSCmdlet.ParameterSetName) { + 'Default' { + @($BaseObject, $OverrideObject) + } + 'Path' { + Write-Verbose "Reading $($Path.Count) file(s) for merge." + foreach ($p in $Path) { + $resolvedPath = Resolve-Path -Path $p -ErrorAction Stop + [System.IO.File]::ReadAllText($resolvedPath.ProviderPath) + } + } + 'LiteralPath' { + Write-Verbose "Reading $($LiteralPath.Count) file(s) for merge." + foreach ($p in $LiteralPath) { + $resolvedPath = Resolve-Path -LiteralPath $p -ErrorAction Stop + [System.IO.File]::ReadAllText($resolvedPath.ProviderPath) + } + } + } + + $merged = (ConvertFrom-Toml -InputObject $documents[0]).Data + for ($i = 1; $i -lt $documents.Count; $i++) { + $override = (ConvertFrom-Toml -InputObject $documents[$i]).Data + Write-Verbose "Merging document $($i + 1) of $($documents.Count) using strategy '$Strategy'." + $merged = Merge-TomlTableObject -Base $merged -Override $override -Strategy $Strategy + } + + return ConvertTo-Toml -InputObject $merged + } +} diff --git a/tests/Toml.Tests.ps1 b/tests/Toml.Tests.ps1 index 37954f3..407101a 100644 --- a/tests/Toml.Tests.ps1 +++ b/tests/Toml.Tests.ps1 @@ -15,6 +15,7 @@ Describe 'Toml' { $commands | Should -Contain 'ConvertTo-Toml' $commands | Should -Contain 'Import-Toml' $commands | Should -Contain 'Export-Toml' + $commands | Should -Contain 'Merge-Toml' $commands | Should -Contain 'Test-Toml' } } @@ -803,6 +804,148 @@ Describe 'Toml' { } } + Describe 'Merge-Toml' { + BeforeAll { + $baseDoc = @' +title = "base" +shared = "base-value" +[server] +host = "localhost" +port = 8080 +[[items]] +name = "base-item" +'@ + + $overrideDoc = @' +extra = "override-only" +shared = "override-value" +[server] +port = 9090 +timeout = 30 +[[items]] +name = "override-item" +'@ + } + + Context 'Return type' { + It 'returns a string' { + $result = Merge-Toml -BaseObject $baseDoc -OverrideObject $overrideDoc + $result | Should -BeOfType [string] + } + + It 'produces output parseable by ConvertFrom-Toml' { + $result = Merge-Toml -BaseObject $baseDoc -OverrideObject $overrideDoc + { ConvertFrom-Toml -InputObject $result } | Should -Not -Throw + } + } + + Context 'Key preservation' { + It 'keeps base-only keys' { + $result = ConvertFrom-Toml -InputObject (Merge-Toml -BaseObject $baseDoc -OverrideObject $overrideDoc) + $result.Data['title'] | Should -Be 'base' + } + + It 'adds override-only keys' { + $result = ConvertFrom-Toml -InputObject (Merge-Toml -BaseObject $baseDoc -OverrideObject $overrideDoc) + $result.Data['extra'] | Should -Be 'override-only' + } + } + + Context 'Strategy: LastWins' { + It 'uses the override value on scalar conflict' { + $result = ConvertFrom-Toml -InputObject (Merge-Toml -BaseObject $baseDoc -OverrideObject $overrideDoc -Strategy 'LastWins') + $result.Data['shared'] | Should -Be 'override-value' + } + + It 'is the default strategy' { + $result = ConvertFrom-Toml -InputObject (Merge-Toml -BaseObject $baseDoc -OverrideObject $overrideDoc) + $result.Data['shared'] | Should -Be 'override-value' + } + } + + Context 'Strategy: FirstWins' { + It 'keeps the base value on scalar conflict' { + $result = ConvertFrom-Toml -InputObject (Merge-Toml -BaseObject $baseDoc -OverrideObject $overrideDoc -Strategy 'FirstWins') + $result.Data['shared'] | Should -Be 'base-value' + } + } + + Context 'Strategy: ErrorOnConflict' { + It 'throws on any duplicate scalar key' { + { Merge-Toml -BaseObject $baseDoc -OverrideObject $overrideDoc -Strategy 'ErrorOnConflict' } | Should -Throw + } + + It 'does not throw when there are no scalar conflicts' { + $noConflict = 'onlyhere = "value"' + { Merge-Toml -BaseObject $baseDoc -OverrideObject $noConflict -Strategy 'ErrorOnConflict' } | Should -Not -Throw + } + } + + Context 'Nested tables' { + It 'deep-merges keys from both sides' { + $result = ConvertFrom-Toml -InputObject (Merge-Toml -BaseObject $baseDoc -OverrideObject $overrideDoc) + $result.Data['server']['host'] | Should -Be 'localhost' + $result.Data['server']['timeout'] | Should -Be 30 + } + + It 'applies the strategy to nested scalar conflicts' { + $result = ConvertFrom-Toml -InputObject (Merge-Toml -BaseObject $baseDoc -OverrideObject $overrideDoc -Strategy 'FirstWins') + $result.Data['server']['port'] | Should -Be 8080 + } + } + + Context 'Arrays of tables' { + It 'concatenates base entries before override entries' { + $result = ConvertFrom-Toml -InputObject (Merge-Toml -BaseObject $baseDoc -OverrideObject $overrideDoc) + $result.Data['items'].Count | Should -Be 2 + $result.Data['items'][0]['name'] | Should -Be 'base-item' + $result.Data['items'][1]['name'] | Should -Be 'override-item' + } + } + + Context 'File input via -Path' { + It 'merges two files in order' { + $basePath = Join-Path $TestDrive 'merge-base.toml' + $overridePath = Join-Path $TestDrive 'merge-override.toml' + Set-Content -Path $basePath -Value $baseDoc -NoNewline + Set-Content -Path $overridePath -Value $overrideDoc -NoNewline + + $result = ConvertFrom-Toml -InputObject (Merge-Toml -Path $basePath, $overridePath) + $result.Data['title'] | Should -Be 'base' + $result.Data['shared'] | Should -Be 'override-value' + } + } + + Context 'File input via -LiteralPath' { + It 'merges two files in order' { + $basePath = Join-Path $TestDrive 'merge-literal-base.toml' + $overridePath = Join-Path $TestDrive 'merge-literal-override.toml' + Set-Content -Path $basePath -Value $baseDoc -NoNewline + Set-Content -Path $overridePath -Value $overrideDoc -NoNewline + + $result = ConvertFrom-Toml -InputObject (Merge-Toml -LiteralPath $basePath, $overridePath) + $result.Data['title'] | Should -Be 'base' + $result.Data['shared'] | Should -Be 'override-value' + } + } + + Context 'Idempotency' { + It 'merging identical documents under FirstWins returns the base document unchanged' { + # Uses a document without arrays of tables, since AoT entries are always + # concatenated regardless of strategy and would not be idempotent. + $scalarOnlyDoc = @' +title = "base" +[server] +host = "localhost" +port = 8080 +'@ + $result = ConvertFrom-Toml -InputObject (Merge-Toml -BaseObject $scalarOnlyDoc -OverrideObject $scalarOnlyDoc -Strategy 'FirstWins') + $expected = ConvertFrom-Toml -InputObject $scalarOnlyDoc + ConvertTo-Toml -InputObject $result.Data | Should -Be (ConvertTo-Toml -InputObject $expected.Data) + } + } + } + Describe 'Test-Toml' { Context 'Module registration' { From 8e594b1752863fe393d32060d58b73d52d369095 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 26 Jul 2026 12:10:37 +0200 Subject: [PATCH 12/13] =?UTF-8?q?=F0=9F=9A=80=20[Minor]:=20Format-Toml=20n?= =?UTF-8?q?ormalizes=20TOML=20to=20canonical=20form=20(#20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Format-Toml` normalizes TOML text into a canonical form — consistent key quoting, canonical scalar formatting, and stable table ordering — without changing its meaning. It is the single-call equivalent of `ConvertFrom-Toml | ConvertTo-Toml`, matching the pattern already established by sister modules `Format-Json` and `Format-Hashtable`. ## New: `Format-Toml` normalizes TOML text ```powershell Get-Content 'Cargo.toml' -Raw | Format-Toml Format-Toml -Path 'Cargo.toml' -Indent 4 Format-Toml -LiteralPath 'C:\configs\[env].toml' ``` `Format-Toml` accepts a TOML string via pipeline (`-InputObject`), or reads a file via `-Path` (wildcard-aware) or `-LiteralPath`. It never writes back to the source file — output is always returned as a `[string]`. Formatting is idempotent: running it twice produces the same result as running it once, and the output always remains parseable by `ConvertFrom-Toml`. TOML has no indentation semantics — nested tables are flat `[a.b]` headers, not indented blocks. The `-Indent` parameter (default `2`) is a display convention: it prefixes nested table headers and their keys with spaces proportional to nesting depth, similar to how the Taplo formatter presents nested tables. Set `-Indent 0` to keep the flat, unindented form. ---
Technical details - New public function `src/functions/public/Format-Toml.ps1`, three parameter sets (`InputObject` default/pipeline, `Path`, `LiteralPath`), reuses `ConvertFrom-Toml` + `ConvertTo-Toml` — no new parser or emitter. - New private helper `src/functions/private/Add-TomlIndentation.ps1` post-processes the canonical (flat) `ConvertTo-Toml` output to add depth-based indentation without touching the existing serializer, so `ConvertTo-Toml`'s own output and tests are unaffected. - Added a `Describe 'Format-Toml'` block to `tests/Toml.Tests.ps1` covering: normalization, semantic equivalence to `ConvertFrom-Toml | ConvertTo-Toml`, indentation at depth, `-Indent 0` flat output, idempotency, round-trip validity, pipeline input, `-Path`, `-LiteralPath`, and error handling for invalid TOML / missing files. - README and `examples/General.ps1` updated with a `Format-Toml` usage section. - Deferred the `Format-Tml` alias mentioned in #3 — the module has no established alias-export mechanism yet (`build.ps1` only exports functions), so adding one alias in isolation would be inconsistent with the rest of the public surface. Can be revisited once alias support lands. - Implementation plan progress (from #3): `Format-Toml` added with tests, README/examples updated. Alias task intentionally deferred (see above). - Verified: `pwsh -File .\build.ps1` then `Invoke-Pester -Path .\tests\Toml.Tests.ps1 -CI` → 132/132 passing. `PSScriptAnalyzer` clean on new files (aside from a pre-existing, module-wide `PSUseBOMForUnicodeEncodedFile` inconsistency already present on most files).
Relevant issues (or links) - Closes PSModule/Toml#3
--- README.md | 15 +++ examples/General.ps1 | 15 +++ src/functions/private/Add-TomlIndentation.ps1 | 75 ++++++++++++ src/functions/public/Format-Toml.ps1 | 108 +++++++++++++++++ tests/Toml.Tests.ps1 | 111 +++++++++++++++++- 5 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 src/functions/private/Add-TomlIndentation.ps1 create mode 100644 src/functions/public/Format-Toml.ps1 diff --git a/README.md b/README.md index 8910eea..e30866c 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,20 @@ $doc.Data['version'] = 2 Export-Toml -InputObject $doc -Path './config.toml' ``` +### Normalize TOML text + +```powershell +Get-Content 'Cargo.toml' -Raw | Format-Toml +Format-Toml -Path 'Cargo.toml' -Indent 4 +``` + +`Format-Toml` parses then re-serializes TOML text, producing consistent key +quoting and canonical scalar forms — equivalent to +`ConvertFrom-Toml | ConvertTo-Toml` as a single call. TOML itself has no +indentation semantics; `-Indent` (default `2`) only controls how many spaces +are used to visually nest table headers and their keys by depth. Set +`-Indent 0` for the flat, unindented form. + ### Merge two TOML documents ```powershell @@ -124,6 +138,7 @@ Merge-Toml -BaseObject $defaults -OverrideObject $overrides | `ConvertTo-Toml` | Serialize object → TOML text | | `Import-Toml` | Read TOML file → `TomlDocument` | | `Export-Toml` | Write object or `TomlDocument` to file | +| `Format-Toml` | Normalize TOML text to canonical form | | `Merge-Toml` | Merge two TOML documents into one | ## Implementation notes diff --git a/examples/General.ps1 b/examples/General.ps1 index d9b484c..877d8f7 100644 --- a/examples/General.ps1 +++ b/examples/General.ps1 @@ -50,6 +50,21 @@ Write-Host $toml # $doc.Data['version'] = 3 # Export-Toml -InputObject $doc -Path './config.toml' +# ── Normalize TOML text ──────────────────────────────────────────────────── +$normalized = Format-Toml -InputObject @' +title="My App" +[server] +host="localhost" +port=8080 +'@ +Write-Host $normalized +# title = "My App" +# +# [server] +# host = "localhost" +# port = 8080 + + # ── Pipeline usage ───────────────────────────────────────────────────────── # '[server] # host = "localhost"' | ConvertFrom-Toml | ConvertTo-Toml diff --git a/src/functions/private/Add-TomlIndentation.ps1 b/src/functions/private/Add-TomlIndentation.ps1 new file mode 100644 index 0000000..d017456 --- /dev/null +++ b/src/functions/private/Add-TomlIndentation.ps1 @@ -0,0 +1,75 @@ +function Add-TomlIndentation { + <# + .SYNOPSIS + Indents nested TOML table sections by nesting depth. + + .DESCRIPTION + Re-indents already-serialized TOML text so that table headers and their + key/value lines are prefixed with spaces proportional to the table's nesting + depth. Depth is derived from the dotted path in each `[path]` / `[[path]]` + header — a root table has depth 1, `[a.b]` has depth 2, and so on. Header + lines are indented one level shallower than the keys they contain, so the + keys visually nest under their header. Root-level keys (before any header) + are never indented. TOML is whitespace-insensitive around keys and headers, + so this is purely a display convention and does not change the parsed value. + + .EXAMPLE + Add-TomlIndentation -Toml "[a]`nx = 1`n[a.b]`ny = 2" -Indent 2 + # Returns: + # [a] + # x = 1 + # [a.b] + # y = 2 + + Indents nested table `[a.b]` and its key two spaces per nesting level. + + .INPUTS + None. Parameters only. + + .OUTPUTS + [string] + #> + [OutputType([string])] + [CmdletBinding()] + param( + # The canonical, non-indented TOML text produced by ConvertTo-Toml. + [Parameter(Mandatory)] + [AllowEmptyString()] + [string] $Toml, + + # Number of spaces to indent per nesting level. 0 disables indentation. + [Parameter(Mandatory)] + [ValidateRange(0, 100)] + [int] $Indent + ) + + if ($Indent -eq 0 -or [string]::IsNullOrEmpty($Toml)) { + return $Toml + } + + $lines = $Toml -split "`n" + $result = [System.Text.StringBuilder]::new() + $depth = 0 + + foreach ($rawLine in $lines) { + $line = $rawLine.TrimEnd("`r") + + if ($line -match '^\[\[(.+)\]\]$' -or $line -match '^\[(.+)\]$') { + $path = $Matches[1] + $depth = (Split-TomlDottedKey -KeyPath $path).Count + $headerIndent = ' ' * ([Math]::Max(0, $depth - 1) * $Indent) + $null = $result.AppendLine("$headerIndent$line") + continue + } + + if ([string]::IsNullOrWhiteSpace($line)) { + $null = $result.AppendLine() + continue + } + + $lineIndent = ' ' * ($depth * $Indent) + $null = $result.AppendLine("$lineIndent$line") + } + + return $result.ToString().TrimEnd() +} diff --git a/src/functions/public/Format-Toml.ps1 b/src/functions/public/Format-Toml.ps1 new file mode 100644 index 0000000..0065576 --- /dev/null +++ b/src/functions/public/Format-Toml.ps1 @@ -0,0 +1,108 @@ +function Format-Toml { + <# + .SYNOPSIS + Normalizes TOML text to a canonical form. + + .DESCRIPTION + Parses TOML text and re-serializes it, producing consistent key quoting, + canonical scalar formatting, and stable table ordering — semantically + equivalent to `ConvertFrom-Toml | ConvertTo-Toml` expressed as a single + ergonomic pipeline step. The result is idempotent: formatting already + canonical text returns the same text unchanged. + + TOML itself is a flat, whitespace-insensitive format — nested tables are + represented as `[a.b]` headers rather than indented blocks, so there is no + spec-defined meaning for indentation. The `-Indent` parameter is offered as + a display convention only: it prefixes each table header and its key/value + lines with spaces proportional to the table's nesting depth (as some TOML + formatters, such as Taplo, do). This never changes the parsed value of the + document. Set `-Indent 0` to disable it and keep the flat, unindented form + that `ConvertTo-Toml` produces. + + .EXAMPLE + Format-Toml -InputObject 'name="value" [ a ] x=1' + + Reformats an inconsistently spaced TOML string into canonical form. + + .EXAMPLE + Get-Content 'Cargo.toml' -Raw | Format-Toml + + Reads a file's content and normalizes it via the pipeline. + + .EXAMPLE + Format-Toml -Path 'Cargo.toml' -Indent 4 + + Reads a TOML file and returns its canonical form with nested tables + indented four spaces per level. + + .EXAMPLE + Format-Toml -LiteralPath 'C:\configs\[env].toml' + + Reads a file whose name contains wildcard-like characters, bypassing + wildcard expansion. + + .INPUTS + [string] — pipeline input supported for the InputObject parameter. + + .OUTPUTS + [string] + + .NOTES + Throws when the input is not valid TOML 1.0.0, when a file path does not + resolve to an existing file, or when a file cannot be read. This function + does not validate — it formats. Use a try/catch (or `Test-Toml`, if + available) to check validity without raising a terminating error. + + .LINK + https://psmodule.io/Toml/Functions/Format-Toml + #> + [OutputType([string])] + [CmdletBinding(DefaultParameterSetName = 'InputObject')] + param( + # TOML text to normalize. + [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'InputObject')] + [ValidateNotNullOrEmpty()] + [string] $InputObject, + + # Path to a TOML file to read and normalize. Accepts relative paths and wildcards. + [Parameter(Mandatory, ParameterSetName = 'Path')] + [ValidateNotNullOrEmpty()] + [string] $Path, + + # Literal path to a TOML file to read and normalize. No wildcard expansion is performed. + [Parameter(Mandatory, ParameterSetName = 'LiteralPath')] + [ValidateNotNullOrEmpty()] + [string] $LiteralPath, + + # Spaces used to indent nested table headers and keys per nesting level. + # This is a display convention only — TOML has no indentation semantics. + # Set to 0 to keep the flat, unindented canonical form. + [Parameter()] + [ValidateRange(0, 100)] + [int] $Indent = 2 + ) + + process { + $tomlText = switch ($PSCmdlet.ParameterSetName) { + 'Path' { + $resolvedPath = Resolve-Path -Path $Path -ErrorAction Stop + Write-Verbose "Reading TOML file: $($resolvedPath.ProviderPath)" + [System.IO.File]::ReadAllText($resolvedPath.ProviderPath) + } + 'LiteralPath' { + $resolvedPath = Resolve-Path -LiteralPath $LiteralPath -ErrorAction Stop + Write-Verbose "Reading TOML file: $($resolvedPath.ProviderPath)" + [System.IO.File]::ReadAllText($resolvedPath.ProviderPath) + } + default { + $InputObject + } + } + + Write-Verbose "Normalizing TOML text ($($tomlText.Length) character(s)), Indent=$Indent." + $doc = ConvertFrom-Toml -InputObject $tomlText + $canonical = ConvertTo-Toml -InputObject $doc + + return Add-TomlIndentation -Toml $canonical -Indent $Indent + } +} diff --git a/tests/Toml.Tests.ps1 b/tests/Toml.Tests.ps1 index 407101a..151df9f 100644 --- a/tests/Toml.Tests.ps1 +++ b/tests/Toml.Tests.ps1 @@ -15,7 +15,7 @@ Describe 'Toml' { $commands | Should -Contain 'ConvertTo-Toml' $commands | Should -Contain 'Import-Toml' $commands | Should -Contain 'Export-Toml' - $commands | Should -Contain 'Merge-Toml' + $commands | Should -Contain 'Format-Toml' $commands | Should -Contain 'Test-Toml' } } @@ -804,6 +804,115 @@ Describe 'Toml' { } } + Describe 'Format-Toml' { + + Context 'Return type' { + It 'returns a string' { + $result = Format-Toml -InputObject 'key = "value"' + $result | Should -BeOfType [string] + } + } + + Context 'Normalization' { + It 'normalizes inconsistent key quoting' { + $result = Format-Toml -InputObject @' +"quoted" = 1 +bare = 2 +'@ + $result | Should -Match 'quoted = 1' + $result | Should -Match 'bare = 2' + } + + It 'is semantically equivalent to ConvertFrom-Toml | ConvertTo-Toml' { + $source = @' +title = "My App" +[server] +port = 8080 +'@ + $expected = ConvertTo-Toml -InputObject (ConvertFrom-Toml -InputObject $source) + (Format-Toml -InputObject $source -Indent 0) | Should -Be $expected + } + + It 'indents nested tables by nesting depth' { + $result = Format-Toml -InputObject "[a]`nx = 1`n[a.b]`ny = 2" -Indent 2 + $result | Should -Match "(?m)^ x = 1\r?$" + $result | Should -Match "(?m)^ \[a\.b\]\r?$" + $result | Should -Match "(?m)^ y = 2\r?$" + } + + It 'produces flat, unindented output when Indent is 0' { + $result = Format-Toml -InputObject "[a]`nx = 1`n[a.b]`ny = 2" -Indent 0 + $result | Should -Match "(?m)^x = 1\r?$" + $result | Should -Match "(?m)^y = 2\r?$" + } + } + + Context 'Idempotency' { + It 'formatting already-canonical text returns the same text' { + $source = @' +title = "My App" +[server] +host = "localhost" +port = 8080 +[server.nested] +key = "value" +'@ + $once = Format-Toml -InputObject $source + $twice = Format-Toml -InputObject $once + $twice | Should -Be $once + } + } + + Context 'Round-trip validity' { + It 'output remains parseable by ConvertFrom-Toml' { + $source = @' +[database] +host = "db.example.com" +ports = [8001, 8002] +'@ + $formatted = Format-Toml -InputObject $source + $parsed = ConvertFrom-Toml -InputObject $formatted + $parsed.Data['database']['host'] | Should -Be 'db.example.com' + $parsed.Data['database']['ports'] | Should -HaveCount 2 + } + } + + Context 'Pipeline' { + It 'accepts InputObject from the pipeline' { + $result = 'key = "value"' | Format-Toml + $result | Should -Match 'key = "value"' + } + } + + Context 'Path parameter' { + It 'reads a file and returns normalized TOML text' { + $path = Join-Path $TestDrive 'input.toml' + Set-Content -Path $path -Value '"quoted"=1' -NoNewline + $result = Format-Toml -Path $path + $result | Should -Match 'quoted = 1' + } + } + + Context 'LiteralPath parameter' { + It 'reads a file and returns normalized TOML text' { + $path = Join-Path $TestDrive 'literal.toml' + Set-Content -Path $path -Value '"quoted"=1' -NoNewline + $result = Format-Toml -LiteralPath $path + $result | Should -Match 'quoted = 1' + } + } + + Context 'Error handling' { + It 'throws on invalid TOML input' { + { Format-Toml -InputObject 'invalid = = "bad"' } | Should -Throw + } + + It 'throws when the file does not exist' { + { Format-Toml -Path 'nonexistent-file.toml' } | Should -Throw + } + } + } + Describe 'Merge-Toml' { BeforeAll { $baseDoc = @' From 4c774e32afc3111f8cea129fa3c9d1ca362e7500 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 26 Jul 2026 12:39:40 +0200 Subject: [PATCH 13/13] Fix CI failures: UTF-8 BOM on all PS1 files, class exporter block, lint fixes - Add UTF-8 BOM to all 31 .ps1 source files (PSUseBOMForUnicodeEncodedFile) - Build psm1 with BOM (fix Lint-Module PSUseBOMForUnicodeEncodedFile on built output) - Add #region Class exporter block to build.ps1 so PSModule.Tests.ps1 recognizes TomlDocument - Replace Write-Host with Write-Output in build.ps1 (PSAvoidUsingWriteHost) - Fix PSUseConsistentWhitespace alignment in build.ps1 variable declarations - Replace Write-Host with Write-Output in examples/General.ps1 - Fix PSAlignAssignmentStatement in examples/General.ps1 hashtable - Fix PSAvoidLongLines in Toml.Tests.ps1 line 331 (use here-string) - Fix MD060 README.md table column alignment, add Test-Toml to commands table Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 17 ++++--- build.ps1 | 50 ++++++++++++++++--- examples/General.ps1 | 10 ++-- src/classes/public/TomlDocument.ps1 | 2 +- src/functions/private/Add-TomlIndentation.ps1 | 2 +- src/functions/private/Add-TomlTableText.ps1 | 2 +- .../ConvertFrom-TomlBasicStringValue.ps1 | 2 +- .../private/ConvertFrom-TomlDateTime.ps1 | 2 +- .../ConvertFrom-TomlLiteralStringValue.ps1 | 2 +- .../private/ConvertFrom-TomlParsedValue.ps1 | 2 +- .../private/ConvertFrom-TomlScalarToken.ps1 | 2 +- .../private/ConvertFrom-TomlTable.ps1 | 2 +- .../private/ConvertFrom-TomlValue.ps1 | 2 +- .../private/ConvertTo-TomlArrayObject.ps1 | 2 +- .../private/ConvertTo-TomlTableObject.ps1 | 2 +- src/functions/private/ConvertTo-TomlValue.ps1 | 2 +- src/functions/private/Format-TomlKey.ps1 | 2 +- src/functions/private/Get-TomlBareToken.ps1 | 2 +- .../private/Get-TomlContentWithoutComment.ps1 | 2 +- src/functions/private/Get-TomlNestedTable.ps1 | 2 +- src/functions/private/Join-TomlKeyPath.ps1 | 2 +- .../private/Merge-TomlTableObject.ps1 | 2 +- src/functions/private/Skip-TomlWhitespace.ps1 | 2 +- src/functions/private/Split-TomlDottedKey.ps1 | 2 +- .../Test-TomlEndsWithDoubleNewLine.ps1 | 2 +- src/functions/private/Test-TomlTableArray.ps1 | 2 +- src/functions/public/ConvertFrom-Toml.ps1 | 2 +- src/functions/public/ConvertTo-Toml.ps1 | 2 +- src/functions/public/Format-Toml.ps1 | 2 +- src/functions/public/Merge-Toml.ps1 | 2 +- src/functions/public/Test-Toml.ps1 | 2 +- tests/BeforeAll.ps1 | 2 +- tests/Toml.Tests.ps1 | 16 +++++- 33 files changed, 100 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index e30866c..1863950 100644 --- a/README.md +++ b/README.md @@ -132,14 +132,15 @@ Merge-Toml -BaseObject $defaults -OverrideObject $overrides ## Commands -| Command | Description | -|-------------------|------------------------------------------| -| `ConvertFrom-Toml` | Parse TOML text → `TomlDocument` | -| `ConvertTo-Toml` | Serialize object → TOML text | -| `Import-Toml` | Read TOML file → `TomlDocument` | -| `Export-Toml` | Write object or `TomlDocument` to file | -| `Format-Toml` | Normalize TOML text to canonical form | -| `Merge-Toml` | Merge two TOML documents into one | +| Command | Description | +|---------------------|------------------------------------------| +| `ConvertFrom-Toml` | Parse TOML text → `TomlDocument` | +| `ConvertTo-Toml` | Serialize object → TOML text | +| `Import-Toml` | Read TOML file → `TomlDocument` | +| `Export-Toml` | Write object or `TomlDocument` to file | +| `Format-Toml` | Normalize TOML text to canonical form | +| `Test-Toml` | Validate TOML text without throwing | +| `Merge-Toml` | Merge two TOML documents into one | ## Implementation notes diff --git a/build.ps1 b/build.ps1 index 8b07cf2..690e867 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,4 +1,4 @@ -#!/usr/bin/env pwsh +#!/usr/bin/env pwsh #Requires -Version 7.0 <# .SYNOPSIS @@ -16,9 +16,9 @@ param( ) $ErrorActionPreference = 'Stop' -$moduleName = 'Toml' -$srcPath = Join-Path $PSScriptRoot 'src' -$outputPath = Join-Path $PSScriptRoot 'output' $moduleName +$moduleName = 'Toml' +$srcPath = Join-Path $PSScriptRoot 'src' +$outputPath = Join-Path $PSScriptRoot 'output' $moduleName # ── clean / create output directory ───────────────────────────────────────── if (Test-Path $outputPath) { @@ -70,10 +70,46 @@ if (Test-Path $finallyPath) { $null = $sb.AppendLine((Get-Content $finallyPath -Raw)) } +# class exporter — registers public classes as type accelerators (required by PSModule framework) +$null = $sb.AppendLine(@' +#region Class exporter +$TypeAcceleratorsClass = [psobject].Assembly.GetType('System.Management.Automation.TypeAccelerators') +$ExistingTypeAccelerators = $TypeAcceleratorsClass::Get +$ExportableEnums = @( +) +$ExportableEnums | ForEach-Object { Write-Verbose "Exporting enum '$($_.FullName)'." } +foreach ($Type in $ExportableEnums) { + if ($Type.FullName -in $ExistingTypeAccelerators.Keys) { + Write-Verbose "Enum already exists [$($Type.FullName)]. Skipping." + } else { + Write-Verbose "Importing enum '$Type'." + $TypeAcceleratorsClass::Add($Type.FullName, $Type) + } +} +$ExportableClasses = @( + [TomlDocument] +) +$ExportableClasses | ForEach-Object { Write-Verbose "Exporting class '$($_.FullName)'." } +foreach ($Type in $ExportableClasses) { + if ($Type.FullName -in $ExistingTypeAccelerators.Keys) { + Write-Verbose "Class already exists [$($Type.FullName)]. Skipping." + } else { + Write-Verbose "Importing class '$Type'." + $TypeAcceleratorsClass::Add($Type.FullName, $Type) + } +} +$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { + foreach ($Type in ($ExportableEnums + $ExportableClasses)) { + $null = $TypeAcceleratorsClass::Remove($Type.FullName) + } +}.GetNewClosure() +#endregion Class exporter +'@) + # Export-ModuleMember $null = $sb.AppendLine("Export-ModuleMember -Function @($($publicFunctions | ForEach-Object { "'$_'" } | Join-String -Separator ', '))") -[System.IO.File]::WriteAllText($psm1Path, $sb.ToString(), [System.Text.UTF8Encoding]::new($false)) +[System.IO.File]::WriteAllText($psm1Path, $sb.ToString(), [System.Text.UTF8Encoding]::new($true)) # ── write manifest ──────────────────────────────────────────────────────────── $psd1Path = Join-Path $outputPath "$moduleName.psd1" @@ -90,5 +126,5 @@ $manifest = @{ } New-ModuleManifest @manifest -Write-Host "Built $moduleName $ModuleVersion -> $outputPath" -Write-Host "Public functions: $($publicFunctions -join ', ')" +Write-Output "Built $moduleName $ModuleVersion -> $outputPath" +Write-Output "Public functions: $($publicFunctions -join ', ')" diff --git a/examples/General.ps1 b/examples/General.ps1 index 877d8f7..6ccbed7 100644 --- a/examples/General.ps1 +++ b/examples/General.ps1 @@ -26,16 +26,16 @@ $doc.Data.database.credentials.user # "admin" # ── Serialize to TOML ────────────────────────────────────────────────────── $toml = ConvertTo-Toml -InputObject ([ordered]@{ - title = 'My Application' - version = 2 - debug = $false - server = [ordered]@{ + title = 'My Application' + version = 2 + debug = $false + server = [ordered]@{ host = '0.0.0.0' port = 8080 } features = @('auth', 'logging', 'metrics') }) -Write-Host $toml +Write-Output $toml # ── Import from file ─────────────────────────────────────────────────────── # $doc = Import-Toml -Path './config.toml' diff --git a/src/classes/public/TomlDocument.ps1 b/src/classes/public/TomlDocument.ps1 index 3a6c46a..390ee63 100644 --- a/src/classes/public/TomlDocument.ps1 +++ b/src/classes/public/TomlDocument.ps1 @@ -1,4 +1,4 @@ -# Represents a parsed TOML document. +# Represents a parsed TOML document. # Exposes the root key-value data as an ordered dictionary and records the # file path when the document was loaded from disk. class TomlDocument { diff --git a/src/functions/private/Add-TomlIndentation.ps1 b/src/functions/private/Add-TomlIndentation.ps1 index d017456..753f3c9 100644 --- a/src/functions/private/Add-TomlIndentation.ps1 +++ b/src/functions/private/Add-TomlIndentation.ps1 @@ -1,4 +1,4 @@ -function Add-TomlIndentation { +function Add-TomlIndentation { <# .SYNOPSIS Indents nested TOML table sections by nesting depth. diff --git a/src/functions/private/Add-TomlTableText.ps1 b/src/functions/private/Add-TomlTableText.ps1 index 2b930a2..6ff982d 100644 --- a/src/functions/private/Add-TomlTableText.ps1 +++ b/src/functions/private/Add-TomlTableText.ps1 @@ -1,4 +1,4 @@ -function Add-TomlTableText { +function Add-TomlTableText { <# .SYNOPSIS Appends TOML text for a table to a string builder. diff --git a/src/functions/private/ConvertFrom-TomlBasicStringValue.ps1 b/src/functions/private/ConvertFrom-TomlBasicStringValue.ps1 index ae35f13..3c4c6fa 100644 --- a/src/functions/private/ConvertFrom-TomlBasicStringValue.ps1 +++ b/src/functions/private/ConvertFrom-TomlBasicStringValue.ps1 @@ -1,4 +1,4 @@ -function ConvertFrom-TomlBasicStringValue { +function ConvertFrom-TomlBasicStringValue { <# .SYNOPSIS Parses a TOML basic string at the current source index. diff --git a/src/functions/private/ConvertFrom-TomlDateTime.ps1 b/src/functions/private/ConvertFrom-TomlDateTime.ps1 index 1b66d94..6a35925 100644 --- a/src/functions/private/ConvertFrom-TomlDateTime.ps1 +++ b/src/functions/private/ConvertFrom-TomlDateTime.ps1 @@ -1,4 +1,4 @@ -function ConvertFrom-TomlDateTime { +function ConvertFrom-TomlDateTime { <# .SYNOPSIS Converts a TOML date/time token to a PowerShell date/time value. diff --git a/src/functions/private/ConvertFrom-TomlLiteralStringValue.ps1 b/src/functions/private/ConvertFrom-TomlLiteralStringValue.ps1 index b51177e..7dacd61 100644 --- a/src/functions/private/ConvertFrom-TomlLiteralStringValue.ps1 +++ b/src/functions/private/ConvertFrom-TomlLiteralStringValue.ps1 @@ -1,4 +1,4 @@ -function ConvertFrom-TomlLiteralStringValue { +function ConvertFrom-TomlLiteralStringValue { <# .SYNOPSIS Parses a TOML literal string at the current source index. diff --git a/src/functions/private/ConvertFrom-TomlParsedValue.ps1 b/src/functions/private/ConvertFrom-TomlParsedValue.ps1 index 5ab978d..9aba1bd 100644 --- a/src/functions/private/ConvertFrom-TomlParsedValue.ps1 +++ b/src/functions/private/ConvertFrom-TomlParsedValue.ps1 @@ -1,4 +1,4 @@ -function ConvertFrom-TomlParsedValue { +function ConvertFrom-TomlParsedValue { <# .SYNOPSIS Parses a TOML value at the current source index. diff --git a/src/functions/private/ConvertFrom-TomlScalarToken.ps1 b/src/functions/private/ConvertFrom-TomlScalarToken.ps1 index 9db48cf..8a53ead 100644 --- a/src/functions/private/ConvertFrom-TomlScalarToken.ps1 +++ b/src/functions/private/ConvertFrom-TomlScalarToken.ps1 @@ -1,4 +1,4 @@ -function ConvertFrom-TomlScalarToken { +function ConvertFrom-TomlScalarToken { <# .SYNOPSIS Converts a scalar TOML token to a PowerShell value. diff --git a/src/functions/private/ConvertFrom-TomlTable.ps1 b/src/functions/private/ConvertFrom-TomlTable.ps1 index a43c3b4..f810cfb 100644 --- a/src/functions/private/ConvertFrom-TomlTable.ps1 +++ b/src/functions/private/ConvertFrom-TomlTable.ps1 @@ -1,4 +1,4 @@ -function ConvertFrom-TomlTable { +function ConvertFrom-TomlTable { <# .SYNOPSIS Parses a TOML document string into an ordered dictionary. diff --git a/src/functions/private/ConvertFrom-TomlValue.ps1 b/src/functions/private/ConvertFrom-TomlValue.ps1 index e765c8f..be38eac 100644 --- a/src/functions/private/ConvertFrom-TomlValue.ps1 +++ b/src/functions/private/ConvertFrom-TomlValue.ps1 @@ -1,4 +1,4 @@ -function ConvertFrom-TomlValue { +function ConvertFrom-TomlValue { <# .SYNOPSIS Converts a TOML value token to a native PowerShell value. diff --git a/src/functions/private/ConvertTo-TomlArrayObject.ps1 b/src/functions/private/ConvertTo-TomlArrayObject.ps1 index da288bc..c37058b 100644 --- a/src/functions/private/ConvertTo-TomlArrayObject.ps1 +++ b/src/functions/private/ConvertTo-TomlArrayObject.ps1 @@ -1,4 +1,4 @@ -function ConvertTo-TomlArrayObject { +function ConvertTo-TomlArrayObject { <# .SYNOPSIS Normalizes a PowerShell enumerable to a TOML-compatible array. diff --git a/src/functions/private/ConvertTo-TomlTableObject.ps1 b/src/functions/private/ConvertTo-TomlTableObject.ps1 index 06ea9ab..dc8cfa5 100644 --- a/src/functions/private/ConvertTo-TomlTableObject.ps1 +++ b/src/functions/private/ConvertTo-TomlTableObject.ps1 @@ -1,4 +1,4 @@ -function ConvertTo-TomlTableObject { +function ConvertTo-TomlTableObject { <# .SYNOPSIS Normalizes input data to TOML-compatible objects. diff --git a/src/functions/private/ConvertTo-TomlValue.ps1 b/src/functions/private/ConvertTo-TomlValue.ps1 index eed39ec..8f0ff94 100644 --- a/src/functions/private/ConvertTo-TomlValue.ps1 +++ b/src/functions/private/ConvertTo-TomlValue.ps1 @@ -1,4 +1,4 @@ -function ConvertTo-TomlValue { +function ConvertTo-TomlValue { <# .SYNOPSIS Converts a normalized value to a TOML literal. diff --git a/src/functions/private/Format-TomlKey.ps1 b/src/functions/private/Format-TomlKey.ps1 index afacffb..e1effc0 100644 --- a/src/functions/private/Format-TomlKey.ps1 +++ b/src/functions/private/Format-TomlKey.ps1 @@ -1,4 +1,4 @@ -function Format-TomlKey { +function Format-TomlKey { <# .SYNOPSIS Formats a key for TOML output. diff --git a/src/functions/private/Get-TomlBareToken.ps1 b/src/functions/private/Get-TomlBareToken.ps1 index 0f2fc6b..e30738f 100644 --- a/src/functions/private/Get-TomlBareToken.ps1 +++ b/src/functions/private/Get-TomlBareToken.ps1 @@ -1,4 +1,4 @@ -function Get-TomlBareToken { +function Get-TomlBareToken { <# .SYNOPSIS Reads a bare TOML token from source. diff --git a/src/functions/private/Get-TomlContentWithoutComment.ps1 b/src/functions/private/Get-TomlContentWithoutComment.ps1 index 5af75ff..baf9788 100644 --- a/src/functions/private/Get-TomlContentWithoutComment.ps1 +++ b/src/functions/private/Get-TomlContentWithoutComment.ps1 @@ -1,4 +1,4 @@ -function Get-TomlContentWithoutComment { +function Get-TomlContentWithoutComment { <# .SYNOPSIS Removes TOML inline comments while preserving quoted text. diff --git a/src/functions/private/Get-TomlNestedTable.ps1 b/src/functions/private/Get-TomlNestedTable.ps1 index a630ff7..19c39a8 100644 --- a/src/functions/private/Get-TomlNestedTable.ps1 +++ b/src/functions/private/Get-TomlNestedTable.ps1 @@ -1,4 +1,4 @@ -function Get-TomlNestedTable { +function Get-TomlNestedTable { <# .SYNOPSIS Resolves or creates nested table path segments. diff --git a/src/functions/private/Join-TomlKeyPath.ps1 b/src/functions/private/Join-TomlKeyPath.ps1 index 5154f46..e2b6808 100644 --- a/src/functions/private/Join-TomlKeyPath.ps1 +++ b/src/functions/private/Join-TomlKeyPath.ps1 @@ -1,4 +1,4 @@ -function Join-TomlKeyPath { +function Join-TomlKeyPath { <# .SYNOPSIS Joins key path segments into a dotted path. diff --git a/src/functions/private/Merge-TomlTableObject.ps1 b/src/functions/private/Merge-TomlTableObject.ps1 index 1629974..c1c9395 100644 --- a/src/functions/private/Merge-TomlTableObject.ps1 +++ b/src/functions/private/Merge-TomlTableObject.ps1 @@ -1,4 +1,4 @@ -function Merge-TomlTableObject { +function Merge-TomlTableObject { <# .SYNOPSIS Recursively merges two TOML table dictionaries. diff --git a/src/functions/private/Skip-TomlWhitespace.ps1 b/src/functions/private/Skip-TomlWhitespace.ps1 index cf3a352..ad522d9 100644 --- a/src/functions/private/Skip-TomlWhitespace.ps1 +++ b/src/functions/private/Skip-TomlWhitespace.ps1 @@ -1,4 +1,4 @@ -function Skip-TomlWhitespace { +function Skip-TomlWhitespace { <# .SYNOPSIS Advances an index past whitespace in a TOML source string. diff --git a/src/functions/private/Split-TomlDottedKey.ps1 b/src/functions/private/Split-TomlDottedKey.ps1 index 68a5cb8..bec2645 100644 --- a/src/functions/private/Split-TomlDottedKey.ps1 +++ b/src/functions/private/Split-TomlDottedKey.ps1 @@ -1,4 +1,4 @@ -function Split-TomlDottedKey { +function Split-TomlDottedKey { <# .SYNOPSIS Splits a TOML dotted key into normalized key segments. diff --git a/src/functions/private/Test-TomlEndsWithDoubleNewLine.ps1 b/src/functions/private/Test-TomlEndsWithDoubleNewLine.ps1 index 1de98de..c8f85c5 100644 --- a/src/functions/private/Test-TomlEndsWithDoubleNewLine.ps1 +++ b/src/functions/private/Test-TomlEndsWithDoubleNewLine.ps1 @@ -1,4 +1,4 @@ -function Test-TomlEndsWithDoubleNewLine { +function Test-TomlEndsWithDoubleNewLine { <# .SYNOPSIS Tests whether a StringBuilder ends with two LF characters. diff --git a/src/functions/private/Test-TomlTableArray.ps1 b/src/functions/private/Test-TomlTableArray.ps1 index 164ddc5..28b6185 100644 --- a/src/functions/private/Test-TomlTableArray.ps1 +++ b/src/functions/private/Test-TomlTableArray.ps1 @@ -1,4 +1,4 @@ -function Test-TomlTableArray { +function Test-TomlTableArray { <# .SYNOPSIS Tests whether a value is an array of TOML tables. diff --git a/src/functions/public/ConvertFrom-Toml.ps1 b/src/functions/public/ConvertFrom-Toml.ps1 index 0f211da..e51e7c3 100644 --- a/src/functions/public/ConvertFrom-Toml.ps1 +++ b/src/functions/public/ConvertFrom-Toml.ps1 @@ -1,4 +1,4 @@ -function ConvertFrom-Toml { +function ConvertFrom-Toml { <# .SYNOPSIS Converts TOML text to a TomlDocument. diff --git a/src/functions/public/ConvertTo-Toml.ps1 b/src/functions/public/ConvertTo-Toml.ps1 index a4b3970..e1a7aa2 100644 --- a/src/functions/public/ConvertTo-Toml.ps1 +++ b/src/functions/public/ConvertTo-Toml.ps1 @@ -1,4 +1,4 @@ -function ConvertTo-Toml { +function ConvertTo-Toml { <# .SYNOPSIS Converts a PowerShell object graph to TOML text. diff --git a/src/functions/public/Format-Toml.ps1 b/src/functions/public/Format-Toml.ps1 index 0065576..8c70213 100644 --- a/src/functions/public/Format-Toml.ps1 +++ b/src/functions/public/Format-Toml.ps1 @@ -1,4 +1,4 @@ -function Format-Toml { +function Format-Toml { <# .SYNOPSIS Normalizes TOML text to a canonical form. diff --git a/src/functions/public/Merge-Toml.ps1 b/src/functions/public/Merge-Toml.ps1 index 1818dd7..a69b056 100644 --- a/src/functions/public/Merge-Toml.ps1 +++ b/src/functions/public/Merge-Toml.ps1 @@ -1,4 +1,4 @@ -function Merge-Toml { +function Merge-Toml { <# .SYNOPSIS Merges two or more TOML documents into one. diff --git a/src/functions/public/Test-Toml.ps1 b/src/functions/public/Test-Toml.ps1 index a06bc93..646e59d 100644 --- a/src/functions/public/Test-Toml.ps1 +++ b/src/functions/public/Test-Toml.ps1 @@ -1,4 +1,4 @@ -function Test-Toml { +function Test-Toml { <# .SYNOPSIS Tests whether a string or file contains valid TOML. diff --git a/tests/BeforeAll.ps1 b/tests/BeforeAll.ps1 index 1981541..4911733 100644 --- a/tests/BeforeAll.ps1 +++ b/tests/BeforeAll.ps1 @@ -1,3 +1,3 @@ -# Shared setup executed once before the test matrix. +# Shared setup executed once before the test matrix. # The module is imported by the CI runner before tests are invoked. # Add any session-wide test fixtures or environment configuration here. diff --git a/tests/Toml.Tests.ps1 b/tests/Toml.Tests.ps1 index 151df9f..0b656bb 100644 --- a/tests/Toml.Tests.ps1 +++ b/tests/Toml.Tests.ps1 @@ -1,4 +1,4 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Required for Pester tests')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Required for Pester tests')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Required for Pester tests')] [CmdletBinding()] param() @@ -328,7 +328,19 @@ Describe 'Toml' { } It 'parses sub-tables independently per array entry' { - $toml = "[[stages]]`nname = `"build`"`n`n [stages.env]`n KEY = `"A`"`n`n[[stages]]`nname = `"test`"`n`n [stages.env]`n KEY = `"B`"" + $toml = @' +[[stages]] +name = "build" + + [stages.env] + KEY = "A" + +[[stages]] +name = "test" + + [stages.env] + KEY = "B" +'@ $result = ConvertFrom-Toml -InputObject $toml $result.Data['stages'][0]['env']['KEY'] | Should -Be 'A' $result.Data['stages'][1]['env']['KEY'] | Should -Be 'B'