From c887394fd8fed27187cd32e18b52b7ade8ed17be Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 9 Jul 2024 21:31:08 -0700 Subject: [PATCH 01/14] Update Helpers.ps1 --- src/Private/Helpers.ps1 | 238 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 236 insertions(+), 2 deletions(-) diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index d9d09ded0..7d1dd026e 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -1689,7 +1689,7 @@ function ConvertTo-PodeResponseContent { } } - { $_ -match '^(.*\/)?(.*\+)?yaml$' } { + { $_ -match '^(.*\/)?(.*\+)?yaml$' } { if ($InputObject -isnot [string]) { if ($Depth -le 0) { return (ConvertTo-PodeYamlInternal -InputObject $InputObject ) @@ -1847,6 +1847,10 @@ function ConvertFrom-PodeRequestContent { $Result.Data = [xml]($Content) } + { $_ -ilike '*/yaml' } { + $Result.Data = ($Content | ConvertFrom-PodeYaml ) + } + { $_ -ilike '*/csv' } { $Result.Data = ($Content | ConvertFrom-Csv) } @@ -4035,4 +4039,234 @@ function Resolve-PodeObjectArray { # For any other type, convert it to a PowerShell object return New-Object psobject -Property $Property } -} \ No newline at end of file +} + +<# +.SYNOPSIS + Converts a YAML input object into a hashtable, using either the internal converter or an external YAML module if available. + +.DESCRIPTION + This function processes a YAML input object and converts it into a hashtable. It checks if a YAML module is available for conversion. + If the module is available, it uses that for conversion. Otherwise, it falls back to the internal converter. + +.PARAMETER InputObject + The YAML input object to be converted. + +.EXAMPLE + $yamlString = @' + openapi: 3.0.3 + info: + title: Async test - OpenAPI 3.0 + version: 0.0.1 + paths: + /task/{taskId}: + get: + summary: Get Pode Task Info + '@ + $hashtable = ConvertFrom-PodeYaml -InputObject $yamlString + # Converts the YAML string to a hashtable. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function ConvertFrom-PodeYaml { + [CmdletBinding()] + [OutputType([string])] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] + [AllowNull()] + $InputObject + ) + + begin { + # Initialize an array to store pipeline objects + $pipelineObject = @() + } + + process { + # Collect objects from the pipeline + $pipelineObject += $_ + } + + end { + # If there are multiple pipeline objects, combine them into a single input object + if ($pipelineObject.Count -gt 1) { + $InputObject = $pipelineObject + } + + # Check if the internal YAML converter should be used + if ($PodeContext.Server.Web.OpenApi.UsePodeYamlInternal) { + return ConvertFrom-PodeYamlInternal -InputObject $InputObject + } + + # Check if a YAML module has been imported, if not, test for available YAML modules + if ($null -eq $PodeContext.Server.InternalCache.YamlModuleImported) { + $PodeContext.Server.InternalCache.YamlModuleImported = ((Test-PodeModuleInstalled -Name 'PSYaml') -or (Test-PodeModuleInstalled -Name 'powershell-yaml')) + } + + # Use the external YAML module if available, otherwise use the internal converter + if ($PodeContext.Server.InternalCache.YamlModuleImported) { + return ($InputObject | ConvertFrom-Yaml) + } + else { + return ConvertFrom-PodeYamlInternal -InputObject $InputObject + } + } +} + + +<# +.SYNOPSIS + Converts a YAML string into a nested ordered hashtable. + +.DESCRIPTION + This function takes a YAML string as input and converts it into a nested ordered hashtable, preserving + the order of keys. It supports conversion of boolean, integer, float, and datetime values. + +.PARAMETER InputObject + The YAML string to be converted. + +.EXAMPLE + $yamlString = @' + openapi: 3.0.3 + info: + title: Async test - OpenAPI 3.0 + version: 0.0.1 + paths: + /task/{taskId}: + get: + summary: Get Pode Task Info + '@ + + $hashtable = ConvertFrom-PodeYamlInternal -InputObject $yamlString + # Converts the YAML string to a nested ordered hashtable. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function ConvertFrom-PodeYamlInternal { + param ( + [Parameter(Mandatory = $true)] + [string]$InputObject + ) + # Split the YAML input into lines + [string[]]$lines = $InputObject -split "`n" + # Initialize the main hashtable as an ordered hashtable + $hashtable = [ordered]@{} + # Stacks to keep track of current hashtable and indentation levels + $stack = [System.Collections.Stack]::new() + $indentStack = [System.Collections.Stack]::new() + $stack.Push($hashtable) + $indentStack.Push(-1) + + # Iterate over each line of the YAML input + for ($i = 0; $i -lt $lines.Length; $i++) { + $line = $lines[$i] + if ($line -match '^(\s*)([^:]+):\s*(.*)$') { + $indent = $matches[1].Length + $key = $matches[2].Trim() + $value = $matches[3].Trim() + + # Pop the stack if the current indentation level is less than or equal to the previous level + while ( $indentStack.Peek() -ge $indent) { + $null = $indentStack.Pop() + $null = $stack.Pop() + } + + # Peek the current hashtable from the stack + $current = $null = $stack.Peek() + + # If value is empty, create a new nested ordered hashtable + if ($value -eq '') { + $current[$key] = [ordered]@{} + $stack.Push($current[$key]) + $indentStack.Push($indent) + } + # Handle inline arrays + elseif ($value -match '^\[(.*)\]$') { + $current[$key] = ($matches[1] -split ',\s*') -replace '"', '' + } + # Handle multiline strings + elseif ($value -eq '|') { + $value = '' + while (++$i -lt $lines.Length -and $lines[$i] -match '^\s+(.*)$') { + $value += $matches[1] + "`n" + } + $i-- + $current[$key] = $value.TrimEnd() + } + # Convert and assign the value + else { + $current[$key] = Convert-PodeStringToType $value + } + } + # Handle list items + elseif ($line -match '^\s*-\s*(.*)$') { + $value = $matches[1].Trim() + if (-not ($current[$key] -is [System.Collections.ArrayList])) { + $current[$key] = [System.Collections.ArrayList]::new() + } + $null = $current[$key].Add((Convert-PodeStringToType $value)) + } + } + + return $hashtable +} + +<# +.SYNOPSIS + Converts a string value to its appropriate data type (bool, int, float, datetime, or string). + +.DESCRIPTION + This function takes a string value and determines if it can be converted to a boolean, integer, float, + or datetime. If none of these conversions apply, it returns the original string. + +.PARAMETER value + The string value to be converted. + +.EXAMPLE + $convertedValue = Convert-PodeStringToType -value "true" + # Converts the string "true" to a boolean $true. + +.EXAMPLE + $convertedValue = Convert-PodeStringToType -value "123" + # Converts the string "123" to an integer 123. + +.EXAMPLE + $convertedValue = Convert-PodeStringToType -value "123.45" + # Converts the string "123.45" to a float 123.45. + +.EXAMPLE + $convertedValue = Convert-PodeStringToType -value "2021-05-01" + # Converts the string "2021-05-01" to a datetime object. + +.EXAMPLE + $convertedValue = Convert-PodeStringToType -value "some string" + # Returns the original string "some string". + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Convert-PodeStringToType($value) { + # Convert to boolean if value matches 'true' or 'false' + if ($value -match '^(true|false)$') { + return [bool]::Parse($value) + } + # Convert to integer if value matches an integer pattern + elseif ($value -match '^-?\d+$') { + return [int]::Parse($value) + } + # Convert to float if value matches a float pattern + elseif ($value -match '^-?\d*\.\d+$') { + return [float]::Parse($value) + } + # Convert to datetime if value matches a datetime pattern + elseif ($value -match '^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{1,3})?(Z|[+-]\d{2}:\d{2})?)?$') { + return [DateTime]::Parse($value) + } + # Return the original string if no other conversion applies + else { + return $value + } +} + From 48bec85601c959cad8017f4054c6019b6c21b774 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 10 Jul 2024 09:33:38 -0700 Subject: [PATCH 02/14] Fix an issue where ConvertTo-PodeYamlInternal was usedinstead of ConvertTo-PodeYaml --- src/Private/Helpers.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 7d1dd026e..8ba7febaf 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -1692,10 +1692,10 @@ function ConvertTo-PodeResponseContent { { $_ -match '^(.*\/)?(.*\+)?yaml$' } { if ($InputObject -isnot [string]) { if ($Depth -le 0) { - return (ConvertTo-PodeYamlInternal -InputObject $InputObject ) + return (ConvertTo-PodeYaml -InputObject $InputObject ) } else { - return (ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth ) + return (ConvertTo-PodeYaml -InputObject $InputObject -Depth $Depth ) } } From f9470ab57e956e11af9cdc2354beec38b018e624 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 12 Jul 2024 11:34:34 -0700 Subject: [PATCH 03/14] Yaml function are now public Added tests --- src/Pode.psd1 | 2 + src/Private/Helpers.ps1 | 132 --------------- src/Public/Utilities.ps1 | 145 ++++++++++++++++ tests/unit/Helpers.Tests.ps1 | 53 +----- tests/unit/Utilities.Tests.ps1 | 292 +++++++++++++++++++++++++++++++++ 5 files changed, 440 insertions(+), 184 deletions(-) create mode 100644 tests/unit/Utilities.Tests.ps1 diff --git a/src/Pode.psd1 b/src/Pode.psd1 index 6df13ad50..30ac81e73 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -141,6 +141,8 @@ 'ConvertFrom-PodeXml', 'Set-PodeDefaultFolder', 'Get-PodeDefaultFolder', + 'ConvertTo-PodeYaml', + 'ConvertFrom-PodeYaml', # routes 'Add-PodeRoute', diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 8ba7febaf..33b8c02d3 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -3629,66 +3629,6 @@ function Test-PodeVersionPwshEOL { } } - -<# -.SYNOPSIS - creates a YAML description of the data in the object - based on https://github.com/Phil-Factor/PSYaml - -.DESCRIPTION - This produces YAML from any object you pass to it. - -.PARAMETER Object - The object that you want scripted out. This parameter accepts input via the pipeline. - -.PARAMETER Depth - The depth that you want your object scripted to - -.EXAMPLE - Get-PodeOpenApiDefinition|ConvertTo-PodeYaml -#> -function ConvertTo-PodeYaml { - [CmdletBinding()] - [OutputType([string])] - param ( - [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] - [AllowNull()] - $InputObject, - - [parameter()] - [int] - $Depth = 16 - ) - - begin { - $pipelineObject = @() - } - - process { - $pipelineObject += $_ - } - - end { - if ($pipelineObject.Count -gt 1) { - $InputObject = $pipelineObject - } - - if ($PodeContext.Server.Web.OpenApi.UsePodeYamlInternal) { - return ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth -NoNewLine - } - - if ($null -eq $PodeContext.Server.InternalCache.YamlModuleImported) { - $PodeContext.Server.InternalCache.YamlModuleImported = ((Test-PodeModuleInstalled -Name 'PSYaml') -or (Test-PodeModuleInstalled -Name 'powershell-yaml')) - } - - if ($PodeContext.Server.InternalCache.YamlModuleImported) { - return ($InputObject | ConvertTo-Yaml) - } - else { - return ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth -NoNewLine - } - } -} - <# .SYNOPSIS Converts PowerShell objects into a YAML-formatted string. @@ -4041,78 +3981,6 @@ function Resolve-PodeObjectArray { } } -<# -.SYNOPSIS - Converts a YAML input object into a hashtable, using either the internal converter or an external YAML module if available. - -.DESCRIPTION - This function processes a YAML input object and converts it into a hashtable. It checks if a YAML module is available for conversion. - If the module is available, it uses that for conversion. Otherwise, it falls back to the internal converter. - -.PARAMETER InputObject - The YAML input object to be converted. - -.EXAMPLE - $yamlString = @' - openapi: 3.0.3 - info: - title: Async test - OpenAPI 3.0 - version: 0.0.1 - paths: - /task/{taskId}: - get: - summary: Get Pode Task Info - '@ - $hashtable = ConvertFrom-PodeYaml -InputObject $yamlString - # Converts the YAML string to a hashtable. - -.NOTES - This is an internal function and may change in future releases of Pode. -#> -function ConvertFrom-PodeYaml { - [CmdletBinding()] - [OutputType([string])] - param ( - [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] - [AllowNull()] - $InputObject - ) - - begin { - # Initialize an array to store pipeline objects - $pipelineObject = @() - } - - process { - # Collect objects from the pipeline - $pipelineObject += $_ - } - - end { - # If there are multiple pipeline objects, combine them into a single input object - if ($pipelineObject.Count -gt 1) { - $InputObject = $pipelineObject - } - - # Check if the internal YAML converter should be used - if ($PodeContext.Server.Web.OpenApi.UsePodeYamlInternal) { - return ConvertFrom-PodeYamlInternal -InputObject $InputObject - } - - # Check if a YAML module has been imported, if not, test for available YAML modules - if ($null -eq $PodeContext.Server.InternalCache.YamlModuleImported) { - $PodeContext.Server.InternalCache.YamlModuleImported = ((Test-PodeModuleInstalled -Name 'PSYaml') -or (Test-PodeModuleInstalled -Name 'powershell-yaml')) - } - - # Use the external YAML module if available, otherwise use the internal converter - if ($PodeContext.Server.InternalCache.YamlModuleImported) { - return ($InputObject | ConvertFrom-Yaml) - } - else { - return ConvertFrom-PodeYamlInternal -InputObject $InputObject - } - } -} <# diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index dde5ff1bb..548449eba 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -1364,4 +1364,149 @@ function ConvertFrom-PodeXml { } return $oHash +} + + + +<# +.SYNOPSIS + Converts a YAML input object into a hashtable, using either the internal converter or an external YAML module if available. + +.DESCRIPTION + This function processes a YAML input object and converts it into a hashtable. It checks if a YAML module is available for conversion. + If the module is available, it uses that for conversion. Otherwise, it falls back to the internal converter. + +.PARAMETER InputObject + The YAML input object to be converted. + +.EXAMPLE + $yamlString = @' + openapi: 3.0.3 + info: + title: Async test - OpenAPI 3.0 + version: 0.0.1 + paths: + /task/{taskId}: + get: + summary: Get Pode Task Info + '@ + $hashtable = ConvertFrom-PodeYaml -InputObject $yamlString + # Converts the YAML string to a hashtable. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function ConvertFrom-PodeYaml { + [CmdletBinding()] + [OutputType([string])] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] + [AllowNull()] + [string[]] + $InputObject + ) + + begin { + # Initialize an array to store pipeline objects + $pipelineObject = @() + } + + process { + # Collect objects from the pipeline + $pipelineObject += $_ + } + + end { + # If there are multiple pipeline objects, combine them into a single input object + if ($pipelineObject.Count -gt 1) { + $InputObject = $pipelineObject + } + + if ( $InputObject.Count -gt 1) { + $obj = $InputObject -join "`n" + } + else { + $obj = $InputObject[0] + } + + # Check if the internal YAML converter should be used + if ($null -eq $PodeContext -or $PodeContext.Server.Web.OpenApi.UsePodeYamlInternal) { + return ConvertFrom-PodeYamlInternal -InputObject $obj + } + + # Check if a YAML module has been imported, if not, test for available YAML modules + if ($null -eq $PodeContext.Server.InternalCache.YamlModuleImported) { + $PodeContext.Server.InternalCache.YamlModuleImported = ((Test-PodeModuleInstalled -Name 'PSYaml') -or (Test-PodeModuleInstalled -Name 'powershell-yaml')) + } + + # Use the external YAML module if available, otherwise use the internal converter + if ($PodeContext.Server.InternalCache.YamlModuleImported) { + return ($obj | ConvertFrom-Yaml) + } + else { + return ConvertFrom-PodeYamlInternal -InputObject $obj + } + } +} + + + + +<# +.SYNOPSIS + creates a YAML description of the data in the object - based on https://github.com/Phil-Factor/PSYaml + +.DESCRIPTION + This produces YAML from any object you pass to it. + +.PARAMETER Object + The object that you want scripted out. This parameter accepts input via the pipeline. + +.PARAMETER Depth + The depth that you want your object scripted to + +.EXAMPLE + Get-PodeOpenApiDefinition|ConvertTo-PodeYaml +#> +function ConvertTo-PodeYaml { + [CmdletBinding()] + [OutputType([string])] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] + [AllowNull()] + $InputObject, + + [parameter()] + [int] + $Depth = 16 + ) + + begin { + $pipelineObject = @() + } + + process { + $pipelineObject += $_ + } + + end { + if ($pipelineObject.Count -gt 1) { + $InputObject = $pipelineObject + } + + if ($null -eq $PodeContext -or $PodeContext.Server.Web.OpenApi.UsePodeYamlInternal) { + return ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth -NoNewLine + } + + if ($null -eq $PodeContext.Server.InternalCache.YamlModuleImported) { + $PodeContext.Server.InternalCache.YamlModuleImported = ((Test-PodeModuleInstalled -Name 'PSYaml') -or (Test-PodeModuleInstalled -Name 'powershell-yaml')) + } + + if ($PodeContext.Server.InternalCache.YamlModuleImported) { + return ($InputObject | ConvertTo-Yaml) + } + else { + return ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth -NoNewLine + } + } } \ No newline at end of file diff --git a/tests/unit/Helpers.Tests.ps1 b/tests/unit/Helpers.Tests.ps1 index e9d9cb76f..8187f034f 100644 --- a/tests/unit/Helpers.Tests.ps1 +++ b/tests/unit/Helpers.Tests.ps1 @@ -1665,58 +1665,7 @@ Describe 'New-PodeCron' { } - - -Describe 'ConvertTo-PodeYaml Tests' { - BeforeAll { - $PodeContext = @{ Server = @{InternalCache = @{} } } - } - Context 'When converting basic types' { - It 'Converts strings correctly' { - $result = 'hello world' | ConvertTo-PodeYaml - $result | Should -Be 'hello world' - } - - It 'Converts arrays correctly' { - $result = @('one', 'two', 'three') | ConvertTo-PodeYaml - $expected = (@' -- one -- two -- three -'@) - $result | Should -Be ($expected.Trim() -Replace "`r`n", "`n") - } - - It 'Converts hashtables correctly' { - $hashTable = [ordered]@{ - key1 = 'value1' - key2 = 'value2' - } - $result = $hashTable | ConvertTo-PodeYaml - $result | Should -Be "key1: value1`nkey2: value2" - } - } - - Context 'When converting complex objects' { - It 'Handles nested hashtables' { - $nestedHash = @{ - parent = @{ - child = 'value' - } - } - $result = $nestedHash | ConvertTo-PodeYaml - - $result | Should -Be "parent: `n child: value" - } - } - - Context 'Error handling' { - It 'Returns empty string for null input' { - $result = $null | ConvertTo-PodeYaml - $result | Should -Be '' - } - } -} + Describe 'ConvertTo-PodeYamlInternal Tests' { diff --git a/tests/unit/Utilities.Tests.ps1 b/tests/unit/Utilities.Tests.ps1 new file mode 100644 index 000000000..8551bd97e --- /dev/null +++ b/tests/unit/Utilities.Tests.ps1 @@ -0,0 +1,292 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] +param() +BeforeAll { + Add-Type -AssemblyName 'System.Net.Http' -ErrorAction SilentlyContinue + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' + function Compare-Hashtable { + param ( + [hashtable]$Hashtable1, + [hashtable]$Hashtable2 + ) + + # Function to compare two hashtable values + function Compare-Value($value1, $value2) { + if ($value1 -is [hashtable] -and $value2 -is [hashtable]) { + return Compare-Hashtable -Hashtable1 $value1 -Hashtable2 $value2 + } + else { + return $value1 -eq $value2 + } + } + + $keys1 = $Hashtable1.Keys + $keys2 = $Hashtable2.Keys + + # Check if both hashtables have the same keys + if ($keys1.Count -ne $keys2.Count) { + return $false + } + + foreach ($key in $keys1) { + if (-not $Hashtable2.ContainsKey($key)) { + return $false + } + if ($Hashtable2[$key] -is [hashtable]) { + if (-not (Compare-Hashtable -Hashtable1 $Hashtable1[$key] -Hashtable2 $Hashtable2[$key])){ + return $false + } + } + elseif (-not (Compare-Value $Hashtable1[$key] $Hashtable2[$key])) { + return $false + } + } + + return $true + } +} + +Describe 'ConvertTo-PodeYaml Tests' { + BeforeAll { + $PodeContext = @{ Server = @{InternalCache = @{} } } + } + Context 'When converting basic types' { + It 'Converts strings correctly' { + $result = 'hello world' | ConvertTo-PodeYaml + $result | Should -Be 'hello world' + } + + It 'Converts arrays correctly' { + $result = @('one', 'two', 'three') | ConvertTo-PodeYaml + $expected = (@' +- one +- two +- three +'@) + $result | Should -Be ($expected.Trim() -Replace "`r`n", "`n") + } + + It 'Converts hashtables correctly' { + $hashTable = [ordered]@{ + key1 = 'value1' + key2 = 'value2' + } + $result = $hashTable | ConvertTo-PodeYaml + $result | Should -Be "key1: value1`nkey2: value2" + } + } + + Context 'When converting complex objects' { + It 'Handles nested hashtables' { + $nestedHash = @{ + parent = @{ + child = 'value' + } + } + $result = $nestedHash | ConvertTo-PodeYaml + + $result | Should -Be "parent: `n child: value" + } + } + + Context 'Error handling' { + It 'Returns empty string for null input' { + $result = $null | ConvertTo-PodeYaml + $result | Should -Be '' + } + } +} + +# Requires -Version 5.5 + +Describe 'ConvertFrom-PodeYaml' { + + Context 'YAML Module' { + BeforeAll { + # Mocking the internal function ConvertFrom-PodeYamlInternal + Mock -CommandName ConvertFrom-PodeYamlInternal -MockWith { + return @{ openapi = '3.0.3'; info = @{ title = 'Async test - OpenAPI 3.0'; version = '0.0.1' }; paths = @{ '/task/{taskId}' = @{ get = @{ summary = 'Get Pode Task Info' } } } } + } + + # Mocking the external function ConvertFrom-Yaml + Mock -CommandName ConvertFrom-Yaml -MockWith { + return @{ openapi = '3.0.3'; info = @{ title = 'Async test - OpenAPI 3.0'; version = '0.0.1' }; paths = @{ '/task/{taskId}' = @{ get = @{ summary = 'Get Pode Task Info' } } } } + } + + # Mocking the Test-PodeModuleInstalled function + Mock -CommandName Test-PodeModuleInstalled -MockWith { return $false } + } + Context 'When no YAML module is available' { + BeforeAll { + $PodeContext = @{ + Server = @{ + Web = @{ + OpenApi = @{ + UsePodeYamlInternal = $true + } + } + InternalCache = @{ + YamlModuleImported = $false + } + } + } + } + + It 'Should use the internal converter if no YAML module is available' { + $yamlString = @' + openapi: 3.0.3 + info: + title: Async test - OpenAPI 3.0 + version: 0.0.1 + paths: + /task/{taskId}: + get: + summary: Get Pode Task Info +'@ + + $result = ConvertFrom-PodeYaml -InputObject $yamlString + + Assert-MockCalled -CommandName ConvertFrom-PodeYamlInternal -Times 1 + Assert-MockCalled -CommandName ConvertFrom-Yaml -Times 0 + $result | Should -BeOfType 'hashtable' + $result.openapi | Should -Be '3.0.3' + } + } + + Context 'When a YAML module is available' { + BeforeAll { + $PodeContext = @{ + Server = @{ + Web = @{ + OpenApi = @{ + UsePodeYamlInternal = $false + } + } + InternalCache = @{ + YamlModuleImported = $true + } + } + } + } + + It 'Should use the external converter if a YAML module is available' { + $yamlString = @' + openapi: 3.0.3 + info: + title: Async test - OpenAPI 3.0 + version: 0.0.1 + paths: + /task/{taskId}: + get: + summary: Get Pode Task Info +'@ + + $result = ConvertFrom-PodeYaml -InputObject $yamlString + + Assert-MockCalled -CommandName ConvertFrom-Yaml -Times 1 + Assert-MockCalled -CommandName ConvertFrom-PodeYamlInternal -Times 0 + $result | Should -BeOfType 'hashtable' + $result.openapi | Should -Be '3.0.3' + } + } + + + } + + Context 'When an objects is provided' { + BeforeAll { + $PodeContext = @{ + Server = @{ + Web = @{ + OpenApi = @{ + UsePodeYamlInternal = $true + } + } + InternalCache = @{ + YamlModuleImported = $false + } + } + } + } + + It 'single input object' { + $yamlString1 = @' + openapi: 3.0.3 + info: + title: Async test - OpenAPI 3.0 + version: 0.0.1 + paths: + /task/{taskId}: + get: + summary: Get Pode Task Info +'@ + + + + $result = $yamlString1 | ConvertFrom-PodeYaml + $result | Should -BeOfType 'System.Collections.Specialized.OrderedDictionary' + Compare-Hashtable -Hashtable1 $result -Hashtable2 ( @{ + openapi = '3.0.3' + info = @{ + title = 'Async test - OpenAPI 3.0' + version = '0.0.1' + } + paths = @{ + '/task/{taskId}' = @{ + get = @{ + summary = 'Get Pode Task Info' + } + } + } + }) | Should -BeTrue + } + } + + Context 'When multiple pipeline objects are provided' { + BeforeAll { + $global:PodeContext = @{ + Server = @{ + Web = @{ + OpenApi = @{ + UsePodeYamlInternal = $true + } + } + InternalCache = @{ + YamlModuleImported = $false + } + } + } + } + + It 'Should combine pipeline objects into a single input object' { + $yamlString1 = @' + openapi: 3.0.3 + info: + title: Async test - OpenAPI 3.0 + version: 0.0.1 + paths: + /task/{taskId}: + get: + summary: Get Pode Task Info +'@ + + $yamlString2 = @' + openapi: 3.0.3 + info: + title: Another test - OpenAPI 3.0 + version: 0.0.2 + paths: + /task/{taskId}: + get: + summary: Get Another Task Info +'@ + + $result = $yamlString1, $yamlString2 | ConvertFrom-PodeYaml + $result | Should -BeOfType 'System.Collections.Specialized.OrderedDictionary' + } + } + +} From 783489598dc0584a776b608ef8ea34ef6aa7b710 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 12 Jul 2024 11:37:27 -0700 Subject: [PATCH 04/14] Update Utilities.ps1 --- src/Public/Utilities.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index 548449eba..113ffed81 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -1459,7 +1459,7 @@ function ConvertFrom-PodeYaml { .DESCRIPTION This produces YAML from any object you pass to it. -.PARAMETER Object +.PARAMETER InputObject The object that you want scripted out. This parameter accepts input via the pipeline. .PARAMETER Depth From e99a12110ffb533e1c9f7d7ed4c020cac2369c81 Mon Sep 17 00:00:00 2001 From: mdaneri <17148649+mdaneri@users.noreply.github.com> Date: Fri, 12 Jul 2024 22:10:29 +0000 Subject: [PATCH 05/14] fix test --- tests/unit/Utilities.Tests.ps1 | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/unit/Utilities.Tests.ps1 b/tests/unit/Utilities.Tests.ps1 index 8551bd97e..175ba22c2 100644 --- a/tests/unit/Utilities.Tests.ps1 +++ b/tests/unit/Utilities.Tests.ps1 @@ -47,6 +47,12 @@ BeforeAll { return $true } + + + # Mocking the external function ConvertFrom-Yaml + function ConvertFrom-Yaml { + return @{ openapi = '3.0.3'; info = @{ title = 'Async test - OpenAPI 3.0'; version = '0.0.1' }; paths = @{ '/task/{taskId}' = @{ get = @{ summary = 'Get Pode Task Info' } } } } + } } Describe 'ConvertTo-PodeYaml Tests' { @@ -247,7 +253,7 @@ Describe 'ConvertFrom-PodeYaml' { Context 'When multiple pipeline objects are provided' { BeforeAll { - $global:PodeContext = @{ + $PodeContext = @{ Server = @{ Web = @{ OpenApi = @{ From 725faed36c08b7486acb976c39d4a726a2735223 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 12 Jul 2024 19:35:47 -0700 Subject: [PATCH 06/14] Update Utilities.Tests.ps1 --- tests/unit/Utilities.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/Utilities.Tests.ps1 b/tests/unit/Utilities.Tests.ps1 index 175ba22c2..15b358654 100644 --- a/tests/unit/Utilities.Tests.ps1 +++ b/tests/unit/Utilities.Tests.ps1 @@ -108,7 +108,7 @@ Describe 'ConvertTo-PodeYaml Tests' { # Requires -Version 5.5 -Describe 'ConvertFrom-PodeYaml' { +Describe 'ConvertFrom-PodeYaml test' { Context 'YAML Module' { BeforeAll { From 03168c9e5858071a118414039bb1c3515e6c193c Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 13 Aug 2024 16:24:54 -0700 Subject: [PATCH 07/14] converted yaml function in C# --- src/Listener/Pode.csproj | 3 + src/Listener/PodeConverter.cs | 319 +++++++++++++++++++++++++++++++++ src/Public/Utilities.ps1 | 14 +- tests/unit/Utilities.Tests.ps1 | 16 +- 4 files changed, 341 insertions(+), 11 deletions(-) create mode 100644 src/Listener/PodeConverter.cs diff --git a/src/Listener/Pode.csproj b/src/Listener/Pode.csproj index ad20a3158..b58deaaea 100644 --- a/src/Listener/Pode.csproj +++ b/src/Listener/Pode.csproj @@ -3,4 +3,7 @@ netstandard2.0;net6.0;net8.0 $(NoWarn);SYSLIB0001 + + + diff --git a/src/Listener/PodeConverter.cs b/src/Listener/PodeConverter.cs new file mode 100644 index 000000000..2509bbe33 --- /dev/null +++ b/src/Listener/PodeConverter.cs @@ -0,0 +1,319 @@ +using System; +using System.Collections; +using System.Collections.Specialized; +using System.Linq; +using System.Management.Automation; +using System.Text; +using System.Text.RegularExpressions; + +namespace Pode +{ + public static class PodeConverter + { + public static string ToYaml(object inputObject, int depth = 10, int nestingLevel = 0, bool noNewLine = false) + { + if (depth < nestingLevel) + { + return string.Empty; + } + + if (inputObject == null) + { + return inputObject is Array ? "[]" : string.Empty; + } + + string padding = new string(' ', nestingLevel * 2); + string type = inputObject.GetType().Name; + + if (inputObject is Array) + { + type = inputObject.GetType().BaseType.Name; + } + + if (type != "String") + { + if (inputObject is OrderedDictionary) + { + type = "ordereddictionary"; + } + else if (inputObject is IList) + { + type = "array"; + } + else if (inputObject is Hashtable) + { + type = "hashtable"; + } + } + + StringBuilder output = new StringBuilder(); + switch (type.ToLower()) + { + case "string": + string stringValue = inputObject.ToString(); + if ((stringValue.Contains("\r\n") || stringValue.Length > 80) && !stringValue.StartsWith("http")) + { + StringBuilder multiline = new StringBuilder("|" + Environment.NewLine); + string[] items = stringValue.Split(new[] { "\n" }, StringSplitOptions.None); + foreach (string item in items) + { + string workingString = item.Replace("\r", ""); + int length = workingString.Length; + int index = 0; + int wrap = 80; + + while (index < length) + { + int breakpoint = wrap; + bool linebreak = false; + + if ((length - index) > wrap) + { + int lastSpaceIndex = workingString.LastIndexOf(' ', index + wrap, wrap); + if (lastSpaceIndex != -1) + { + breakpoint = lastSpaceIndex - index; + } + else + { + linebreak = true; + breakpoint--; + } + } + else + { + breakpoint = length - index; + } + + multiline.Append(padding).Append(workingString.Substring(index, breakpoint).Trim()); + if (linebreak) + { + multiline.Append('\\'); + } + + index += breakpoint; + if (index < length) + { + multiline.Append(Environment.NewLine); + } + } + + multiline.Append(Environment.NewLine); + } + + output.Append(multiline.ToString().TrimEnd()); + } + else + { + if (stringValue.StartsWith("#") || stringValue.StartsWith("[") || stringValue.StartsWith("]") || stringValue.StartsWith("@") || stringValue.StartsWith("{") || stringValue.StartsWith("}") || stringValue.StartsWith("!") || stringValue.StartsWith("*")) + { + output.AppendFormat("'{0}'", stringValue.Replace("'", "''")); + } + else + { + output.Append(stringValue); + } + } + break; + + case "hashtable": + case "ordereddictionary": + if (inputObject is IDictionary dict && dict.Count > 0) + { + int index = 0; + StringBuilder stringBuilder = new StringBuilder(); + foreach (DictionaryEntry item in dict) + { + string newPadding = noNewLine && index++ == 0 ? string.Empty : Environment.NewLine + padding; + stringBuilder.Append(newPadding).Append(item.Key).Append(": "); + if (item.Value is ValueType) + { + if (item.Value is bool) + { + stringBuilder.Append(item.Value.ToString().ToLower()); + } + else + { + stringBuilder.Append(item.Value); + } + } + else + { + int increment = item.Value is string ? 2 : 1; + stringBuilder.Append(ToYaml(item.Value, depth, nestingLevel + increment)); + } + } + output.Append(stringBuilder.ToString()); + } + else + { + output.Append("{}"); + } + break; + + case "pscustomobject": + if (inputObject is PSObject psObject && psObject.Properties.Any()) + { + int index = 0; + StringBuilder stringBuilder = new StringBuilder(); + foreach (PSPropertyInfo item in psObject.Properties) + { + string newPadding = noNewLine && index++ == 0 ? string.Empty : Environment.NewLine + padding; + stringBuilder.Append(newPadding).Append(item.Name).Append(": "); + if (item.Value is ValueType) + { + if (item.Value is bool) + { + stringBuilder.Append(item.Value.ToString().ToLower()); + } + else + { + stringBuilder.Append(item.Value); + } + } + else + { + int increment = item.Value is string ? 2 : 1; + stringBuilder.Append(ToYaml(item.Value, depth, nestingLevel + increment)); + } + } + output.Append(stringBuilder.ToString()); + } + else + { + output.Append("{}"); + } + break; + + case "array": + IList list = inputObject as IList; + if (list != null && list.Count == 0) + { + output.Append("[]"); + } + else + { + StringBuilder arrayStringBuilder = new StringBuilder(); + int arrayIndex = 0; + foreach (object item in list) + { + string newPadding = noNewLine && arrayIndex++ == 0 ? string.Empty : Environment.NewLine + padding; + arrayStringBuilder.Append(newPadding).Append("- ").Append(ToYaml(item, depth, nestingLevel + 1,true).Trim('\'')); + } + output.Append(arrayStringBuilder.ToString()); + } + break; + + default: + output.AppendFormat("'{0}'", inputObject); + break; + } + + return output.ToString(); + } + + + public static OrderedDictionary FromYaml(string inputObject) + { + // Split the YAML input into lines + string[] lines = inputObject.Split(new[] { "\n" }, StringSplitOptions.None); + // Initialize the main hashtable as an ordered hashtable + var hashtable = new OrderedDictionary(); + // Stacks to keep track of current hashtable and indentation levels + var stack = new Stack(); + var indentStack = new Stack(); + stack.Push(hashtable); + indentStack.Push(-1); + + // Regex patterns for matching lines + var keyValuePattern = new Regex(@"^(\s*)([^:]+):\s*(.*)$"); + var arrayPattern = new Regex(@"^\[(.*)\]$"); + var multilinePattern = new Regex(@"^\s+(.*)$"); + var listItemPattern = new Regex(@"^\s*-\s*(.*)$"); + + // Variables to keep track of current key and hashtable + OrderedDictionary current = hashtable; + string currentKey = null; + + // Iterate over each line of the YAML input + for (int i = 0; i < lines.Length; i++) + { + string line = lines[i]; + Match match = keyValuePattern.Match(line); + if (match.Success) + { + int indent = match.Groups[1].Length; // Indentation level + string key = match.Groups[2].Value.Trim(); // Key + string value = match.Groups[3].Value.Trim(); // Value + + // Pop the stack if the current indentation level is less than or equal to the previous level + while ((int)indentStack.Peek() >= indent) + { + indentStack.Pop(); + stack.Pop(); + } + + // Peek the current hashtable from the stack + current = (OrderedDictionary)stack.Peek(); + currentKey = key; + + // If value is empty, create a new nested ordered hashtable + if (string.IsNullOrEmpty(value)) + { + var newDict = new OrderedDictionary(); + current[key] = newDict; + stack.Push(newDict); + indentStack.Push(indent); + } + // Handle inline arrays + else if (arrayPattern.IsMatch(value)) + { + current[key] = arrayPattern.Match(value).Groups[1].Value.Split(new[] { ", " }, StringSplitOptions.None); + } + // Handle multiline strings + else if (value == "|") + { + value = string.Empty; + while (++i < lines.Length && multilinePattern.IsMatch(lines[i])) + { + value += multilinePattern.Match(lines[i]).Groups[1].Value + "\n"; + } + i--; + current[key] = value.TrimEnd(); + } + // Convert and assign the value + else + { + current[key] = ConvertPodeStringToType(value); + } + } + // Handle list items + else if (listItemPattern.IsMatch(line)) + { + string value = listItemPattern.Match(line).Groups[1].Value.Trim(); + if (!(current[currentKey] is ArrayList list)) + { + list = new ArrayList(); + current[currentKey] = list; + } + list.Add(ConvertPodeStringToType(value)); + } + } + + return hashtable; + } + + + private static object ConvertPodeStringToType(string value) + { + if (bool.TryParse(value, out bool boolResult)) return boolResult; + if (int.TryParse(value, out int intResult)) return intResult; + if (double.TryParse(value, out double doubleResult)) return doubleResult; + if (DateTime.TryParse(value, out DateTime dateResult)) return dateResult; + return value; + } + } + + + +} diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index 113ffed81..026cd3703 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -1398,7 +1398,7 @@ function ConvertFrom-PodeXml { #> function ConvertFrom-PodeYaml { [CmdletBinding()] - [OutputType([string])] + [OutputType([ordered])] param ( [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] [AllowNull()] @@ -1431,7 +1431,8 @@ function ConvertFrom-PodeYaml { # Check if the internal YAML converter should be used if ($null -eq $PodeContext -or $PodeContext.Server.Web.OpenApi.UsePodeYamlInternal) { - return ConvertFrom-PodeYamlInternal -InputObject $obj + # return ConvertFrom-PodeYamlInternal -InputObject $obj + return [Pode.PodeConverter]::FromYaml($obj) } # Check if a YAML module has been imported, if not, test for available YAML modules @@ -1444,7 +1445,8 @@ function ConvertFrom-PodeYaml { return ($obj | ConvertFrom-Yaml) } else { - return ConvertFrom-PodeYamlInternal -InputObject $obj + return [Pode.PodeConverter]::FromYaml($obj) + # return ConvertFrom-PodeYamlInternal -InputObject $obj } } } @@ -1495,7 +1497,8 @@ function ConvertTo-PodeYaml { } if ($null -eq $PodeContext -or $PodeContext.Server.Web.OpenApi.UsePodeYamlInternal) { - return ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth -NoNewLine + # return ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth -NoNewLine + return [Pode.PodeConverter]::ToYaml($InputObject, $Depth, 0, $true) } if ($null -eq $PodeContext.Server.InternalCache.YamlModuleImported) { @@ -1506,7 +1509,8 @@ function ConvertTo-PodeYaml { return ($InputObject | ConvertTo-Yaml) } else { - return ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth -NoNewLine + # return ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth -NoNewLine + return [Pode.PodeConverter]::ToYaml($InputObject, $Depth, 0, $true) } } } \ No newline at end of file diff --git a/tests/unit/Utilities.Tests.ps1 b/tests/unit/Utilities.Tests.ps1 index 15b358654..88b80107b 100644 --- a/tests/unit/Utilities.Tests.ps1 +++ b/tests/unit/Utilities.Tests.ps1 @@ -3,10 +3,14 @@ param() BeforeAll { Add-Type -AssemblyName 'System.Net.Http' -ErrorAction SilentlyContinue + $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' + if (!([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Pode' })) { + Add-Type -LiteralPath "$($src)/Libs/netstandard2.0/Pode.dll" -ErrorAction Stop + } function Compare-Hashtable { param ( [hashtable]$Hashtable1, @@ -72,7 +76,7 @@ Describe 'ConvertTo-PodeYaml Tests' { - two - three '@) - $result | Should -Be ($expected.Trim() -Replace "`r`n", "`n") + $result | Should -Be $expected.Trim()# -Replace "`r`n", "`n") } It 'Converts hashtables correctly' { @@ -81,7 +85,7 @@ Describe 'ConvertTo-PodeYaml Tests' { key2 = 'value2' } $result = $hashTable | ConvertTo-PodeYaml - $result | Should -Be "key1: value1`nkey2: value2" + $result | Should -Be "key1: value1$([Environment]::NewLine)key2: value2" } } @@ -94,7 +98,7 @@ Describe 'ConvertTo-PodeYaml Tests' { } $result = $nestedHash | ConvertTo-PodeYaml - $result | Should -Be "parent: `n child: value" + $result | Should -Be "parent: $([Environment]::NewLine) child: value" } } @@ -155,9 +159,9 @@ Describe 'ConvertFrom-PodeYaml test' { $result = ConvertFrom-PodeYaml -InputObject $yamlString - Assert-MockCalled -CommandName ConvertFrom-PodeYamlInternal -Times 1 + # Assert-MockCalled -CommandName ConvertFrom-PodeYamlInternal -Times 1 Assert-MockCalled -CommandName ConvertFrom-Yaml -Times 0 - $result | Should -BeOfType 'hashtable' + $result | Should -BeOfType 'ordered' $result.openapi | Should -Be '3.0.3' } } @@ -193,7 +197,7 @@ Describe 'ConvertFrom-PodeYaml test' { $result = ConvertFrom-PodeYaml -InputObject $yamlString Assert-MockCalled -CommandName ConvertFrom-Yaml -Times 1 - Assert-MockCalled -CommandName ConvertFrom-PodeYamlInternal -Times 0 + # Assert-MockCalled -CommandName ConvertFrom-PodeYamlInternal -Times 0 $result | Should -BeOfType 'hashtable' $result.openapi | Should -Be '3.0.3' } From 31067ccad00d95f3bfaf9c007003661661a768b9 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 14 Aug 2024 12:31:02 -0700 Subject: [PATCH 08/14] Changed default Json conversion to PSCustomObject Added options to change the default behavior --- docs/Tutorials/Configuration.md | 5 ++++- examples/server.psd1 | 22 +++++++++++++++++++- src/Private/Context.ps1 | 6 ++++++ src/Private/Helpers.ps1 | 10 ++++++--- src/Public/Utilities.ps1 | 36 ++++++++++++++++++++++----------- 5 files changed, 62 insertions(+), 17 deletions(-) diff --git a/docs/Tutorials/Configuration.md b/docs/Tutorials/Configuration.md index 6b18e04fa..87acf6780 100644 --- a/docs/Tutorials/Configuration.md +++ b/docs/Tutorials/Configuration.md @@ -86,4 +86,7 @@ A "path" like `Server.Ssl.Protocols` looks like the below in the file: | Web.Compression | Sets any compression to use on the Response | [link](../Compression/Responses) | | Web.ContentType | Define expected Content Types for certain Routes | [link](../Routes/Utilities/ContentTypes) | | Web.ErrorPages | Defines configuration for custom error pages | [link](../Routes/Utilities/ErrorPages) | -| Web.Static | Defines configuration for static content, such as caching | [link](../Routes/Utilities/StaticContent) | \ No newline at end of file +| Web.Static | Defines configuration for static content, such as caching | [link](../Routes/Utilities/StaticContent) | +| Web.Conversion.JsonToHashTable | Set the default Json conversion to HashTable if true otherwise PSCustomObject (Default: True for PSCore and False for Windows Powershell )| | +| Web.Conversion.YamlToHashTable | Set the default Yaml conversion to HashTable if true otherwise PSCustomObject (Default: True)| | + \ No newline at end of file diff --git a/examples/server.psd1 b/examples/server.psd1 index d1858842e..aa26bdcd5 100644 --- a/examples/server.psd1 +++ b/examples/server.psd1 @@ -1,5 +1,5 @@ @{ - Web = @{ + Web = @{ Static = @{ Defaults = @( 'index.html', @@ -19,6 +19,7 @@ Default = 'application/html' Routes = @{ '/john' = 'application/json' + '/auth' = 'application/json' } } Compression = @{ @@ -27,7 +28,14 @@ OpenApi = @{ UsePodeYamlInternal = $true } + + Conversion = @{ + JsonToHashTable = $true + XmlToHashTable = $true + YamlToHashTable = $true + } } + Server = @{ FileMonitor = @{ Enable = $false @@ -64,5 +72,17 @@ Enable = $true } } + AsyncRoutes = @{ + HouseKeeping = @{ + TimerInterval = 30 + RetentionMinutes = 10 + } + } + Tasks = @{ + HouseKeeping = @{ + TimerInterval = 30 + RetentionMinutes = 1 + } + } } } \ No newline at end of file diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 3e3395551..d58ba2dec 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -939,6 +939,12 @@ function Set-PodeWebConfiguration { OpenApi = @{ DefaultDefinitionTag = [string](Protect-PodeValue -Value $Configuration.OpenApi.DefaultDefinitionTag -Default 'default') } + Conversion = @{ + # If Pode is running in Powershell Core Json conversion are by default to HashTable + JsonToHashTable = [bool] (Protect-PodeValue -Value $Configuration.Conversion.JsonToHashTable -Default (Test-PodeIsPSCore) ) + XmlToHashTable = [bool] (Protect-PodeValue -Value $Configuration.Conversion.XmlToHashTable -Default $true ) + YamlToHashTable = [bool] (Protect-PodeValue -Value $Configuration.Conversion.XmlToHashTable -Default $true ) + } } if ($Configuration.OpenApi -and $Configuration.OpenApi.ContainsKey('UsePodeYamlInternal')) { diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 33b8c02d3..e7ac680a6 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -1836,10 +1836,14 @@ function ConvertFrom-PodeRequestContent { switch ($ContentType) { { $_ -ilike '*/json' } { if (Test-PodeIsPSCore) { - $Result.Data = ($Content | ConvertFrom-Json -AsHashtable) + $Result.Data = ($Content | ConvertFrom-Json -AsHashtable:$PodeContext.Server.Web.Conversion.JsonToHashTable) } else { - $Result.Data = ($Content | ConvertFrom-Json) + if($PodeContext.Server.Web.Conversion.JsonToHashTable){ + $Result.Data = ConvertTo-PodeHashtable -PSObject ($Content | ConvertFrom-Json) + }else{ + $Result.Data = ($Content | ConvertFrom-Json) + } } } @@ -1848,7 +1852,7 @@ function ConvertFrom-PodeRequestContent { } { $_ -ilike '*/yaml' } { - $Result.Data = ($Content | ConvertFrom-PodeYaml ) + $Result.Data = ($Content | ConvertFrom-PodeYaml -AsHashtable:$PodeContext.Server.Web.Conversion.YamlToHashTable) } { $_ -ilike '*/csv' } { diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index 026cd3703..6d4afcf62 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -1379,6 +1379,9 @@ function ConvertFrom-PodeXml { .PARAMETER InputObject The YAML input object to be converted. +.PARAMETER AsHashTable + Converts the YAML to a hash table object + .EXAMPLE $yamlString = @' openapi: 3.0.3 @@ -1400,10 +1403,14 @@ function ConvertFrom-PodeYaml { [CmdletBinding()] [OutputType([ordered])] param ( - [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] [AllowNull()] [string[]] - $InputObject + $InputObject, + + [Parameter()] + [switch] + $AsHashTable ) begin { @@ -1431,23 +1438,28 @@ function ConvertFrom-PodeYaml { # Check if the internal YAML converter should be used if ($null -eq $PodeContext -or $PodeContext.Server.Web.OpenApi.UsePodeYamlInternal) { - # return ConvertFrom-PodeYamlInternal -InputObject $obj - return [Pode.PodeConverter]::FromYaml($obj) + # return ConvertFrom-PodeYamlInternal -InputObject $obj + $result = [Pode.PodeConverter]::FromYaml($obj) } # Check if a YAML module has been imported, if not, test for available YAML modules - if ($null -eq $PodeContext.Server.InternalCache.YamlModuleImported) { + elseif ($null -eq $PodeContext.Server.InternalCache.YamlModuleImported) { $PodeContext.Server.InternalCache.YamlModuleImported = ((Test-PodeModuleInstalled -Name 'PSYaml') -or (Test-PodeModuleInstalled -Name 'powershell-yaml')) } - - # Use the external YAML module if available, otherwise use the internal converter - if ($PodeContext.Server.InternalCache.YamlModuleImported) { - return ($obj | ConvertFrom-Yaml) + if (!$result) { + # Use the external YAML module if available, otherwise use the internal converter + if ($PodeContext.Server.InternalCache.YamlModuleImported) { + $result = ($obj | ConvertFrom-Yaml) + } + else { + $result = [Pode.PodeConverter]::FromYaml($obj) + # return ConvertFrom-PodeYamlInternal -InputObject $obj + } } - else { - return [Pode.PodeConverter]::FromYaml($obj) - # return ConvertFrom-PodeYamlInternal -InputObject $obj + if ($AsHashTable){ + return [PSCustomObject]$result } + return $result } } From c76ab58c83fa4330794da6f98cb554e0bdd7488d Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 14 Aug 2024 22:11:06 -0700 Subject: [PATCH 09/14] Fix an issue with Windows Powershell --- src/Listener/Pode.csproj | 11 +++- src/Listener/PodeConverter.cs | 5 +- src/Private/Helpers.ps1 | 13 ++-- src/Public/Utilities.ps1 | 112 ++++++++++++++++++++------------- tests/unit/Helpers.Tests.ps1 | 8 +-- tests/unit/Utilities.Tests.ps1 | 28 +++++++-- 6 files changed, 115 insertions(+), 62 deletions(-) diff --git a/src/Listener/Pode.csproj b/src/Listener/Pode.csproj index b58deaaea..ea44b64d2 100644 --- a/src/Listener/Pode.csproj +++ b/src/Listener/Pode.csproj @@ -3,7 +3,14 @@ netstandard2.0;net6.0;net8.0 $(NoWarn);SYSLIB0001 - - + + + + + + + + + diff --git a/src/Listener/PodeConverter.cs b/src/Listener/PodeConverter.cs index 2509bbe33..cf6a7f93c 100644 --- a/src/Listener/PodeConverter.cs +++ b/src/Listener/PodeConverter.cs @@ -1,3 +1,5 @@ +#if !NETSTANDARD2_0 + using System; using System.Collections; using System.Collections.Specialized; @@ -198,7 +200,7 @@ public static string ToYaml(object inputObject, int depth = 10, int nestingLevel foreach (object item in list) { string newPadding = noNewLine && arrayIndex++ == 0 ? string.Empty : Environment.NewLine + padding; - arrayStringBuilder.Append(newPadding).Append("- ").Append(ToYaml(item, depth, nestingLevel + 1,true).Trim('\'')); + arrayStringBuilder.Append(newPadding).Append("- ").Append(ToYaml(item, depth, nestingLevel + 1, true).Trim('\'')); } output.Append(arrayStringBuilder.ToString()); } @@ -317,3 +319,4 @@ private static object ConvertPodeStringToType(string value) } +#endif \ No newline at end of file diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index e7ac680a6..84294bc5b 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -1839,9 +1839,10 @@ function ConvertFrom-PodeRequestContent { $Result.Data = ($Content | ConvertFrom-Json -AsHashtable:$PodeContext.Server.Web.Conversion.JsonToHashTable) } else { - if($PodeContext.Server.Web.Conversion.JsonToHashTable){ + if ($PodeContext.Server.Web.Conversion.JsonToHashTable) { $Result.Data = ConvertTo-PodeHashtable -PSObject ($Content | ConvertFrom-Json) - }else{ + } + else { $Result.Data = ($Content | ConvertFrom-Json) } } @@ -3731,7 +3732,7 @@ function ConvertTo-PodeYamlInternal { 'string' { $String = "$InputObject" if (($string -match '[\r\n]' -or $string.Length -gt 80) -and ($string -notlike 'http*')) { - $multiline = [System.Text.StringBuilder]::new("|`n") + $multiline = [System.Text.StringBuilder]::new('|' + [Environment]::NewLine) $items = $string.Split("`n") for ($i = 0; $i -lt $items.Length; $i++) { @@ -3794,7 +3795,7 @@ function ConvertTo-PodeYamlInternal { $index = 0 $string = [System.Text.StringBuilder]::new() foreach ($item in $InputObject.Keys) { - if ($NoNewLine -and $index++ -eq 0) { $NewPadding = '' } else { $NewPadding = "`n$padding" } + if ($NoNewLine -and $index++ -eq 0) { $NewPadding = '' } else { $NewPadding = [Environment]::NewLine + $padding } $null = $string.Append( $NewPadding).Append( $item).Append(': ') if ($InputObject[$item] -is [System.ValueType]) { if ($InputObject[$item] -is [bool]) { @@ -3820,7 +3821,7 @@ function ConvertTo-PodeYamlInternal { $index = 0 $string = [System.Text.StringBuilder]::new() foreach ($item in ($InputObject | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name)) { - if ($NoNewLine -and $index++ -eq 0) { $NewPadding = '' } else { $NewPadding = "`n$padding" } + if ($NoNewLine -and $index++ -eq 0) { $NewPadding = '' } else { $NewPadding = [Environment]::NewLine + $padding } $null = $string.Append( $NewPadding).Append( $item).Append(': ') if ($InputObject.$item -is [System.ValueType]) { if ($InputObject.$item -is [bool]) { @@ -3845,7 +3846,7 @@ function ConvertTo-PodeYamlInternal { $string = [System.Text.StringBuilder]::new() $index = 0 foreach ($item in $InputObject ) { - if ($NoNewLine -and $index++ -eq 0) { $NewPadding = '' } else { $NewPadding = "`n$padding" } + if ($NoNewLine -and $index++ -eq 0) { $NewPadding = '' } else { $NewPadding = [Environment]::NewLine +$padding } $null = $string.Append($NewPadding).Append('- ').Append((ConvertTo-PodeYamlInternal -InputObject $item -depth $Depth -NestingLevel ($NestingLevel + 1) -NoNewLine)) } $string.ToString() diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index 6d4afcf62..984f3fcdc 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -1401,7 +1401,7 @@ function ConvertFrom-PodeXml { #> function ConvertFrom-PodeYaml { [CmdletBinding()] - [OutputType([ordered])] + [OutputType([System.Collections.Specialized.OrderedDictionary])] param ( [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] [AllowNull()] @@ -1414,52 +1414,60 @@ function ConvertFrom-PodeYaml { ) begin { - # Initialize an array to store pipeline objects + # Initialize an array to store pipeline objects for later processing. $pipelineObject = @() + + # Determine if the internal Pode YAML converter should be used. + $usePodeYamlInternal = $null -eq $PodeContext -or $PodeContext.Server.Web.OpenApi.UsePodeYamlInternal + + # Check if the YAML module has already been imported and cache the result. + $yamlModuleImported = $PodeContext.Server.InternalCache.YamlModuleImported + if ($null -eq $yamlModuleImported) { + # Test if either PSYaml or powershell-yaml module is installed. + $yamlModuleImported = ((Test-PodeModuleInstalled -Name 'PSYaml') -or (Test-PodeModuleInstalled -Name 'powershell-yaml')) + # Cache the result of the module check. + $PodeContext.Server.InternalCache.YamlModuleImported = $yamlModuleImported + } } process { - # Collect objects from the pipeline + # Add each pipeline object to the array for processing in the 'end' block. $pipelineObject += $_ } end { - # If there are multiple pipeline objects, combine them into a single input object - if ($pipelineObject.Count -gt 1) { - $InputObject = $pipelineObject - } - + # If multiple objects were passed through the pipeline, join them into a single string. if ( $InputObject.Count -gt 1) { - $obj = $InputObject -join "`n" + $obj = $InputObject -join [Environment]::NewLine } else { $obj = $InputObject[0] } - # Check if the internal YAML converter should be used - if ($null -eq $PodeContext -or $PodeContext.Server.Web.OpenApi.UsePodeYamlInternal) { - # return ConvertFrom-PodeYamlInternal -InputObject $obj - $result = [Pode.PodeConverter]::FromYaml($obj) - } - - # Check if a YAML module has been imported, if not, test for available YAML modules - elseif ($null -eq $PodeContext.Server.InternalCache.YamlModuleImported) { - $PodeContext.Server.InternalCache.YamlModuleImported = ((Test-PodeModuleInstalled -Name 'PSYaml') -or (Test-PodeModuleInstalled -Name 'powershell-yaml')) - } - if (!$result) { - # Use the external YAML module if available, otherwise use the internal converter - if ($PodeContext.Server.InternalCache.YamlModuleImported) { - $result = ($obj | ConvertFrom-Yaml) + # Determine which YAML conversion method to use. + if ($usePodeYamlInternal -or -not $yamlModuleImported) { + # If using the internal Pode YAML converter and PowerShell Core, use the Pode.PodeConverter class. + if (Test-PodeIsPSCore) { + $result = [Pode.PodeConverter]::FromYaml($obj) } else { - $result = [Pode.PodeConverter]::FromYaml($obj) - # return ConvertFrom-PodeYamlInternal -InputObject $obj + # Use the internal Pode YAML converter for Windows PowerShell. + $result = ConvertFrom-PodeYamlInternal -InputObject $obj } } - if ($AsHashTable){ + else { + # If an external YAML module is available, use it for conversion. + $result = ($InputObject | ConvertFrom-Yaml) + } + + # Convert the result to a hashtable if the AsHashTable switch is used. + if ($AsHashTable) { return [PSCustomObject]$result } + + # Return the resulting object. return $result + } } @@ -1468,38 +1476,54 @@ function ConvertFrom-PodeYaml { <# .SYNOPSIS - creates a YAML description of the data in the object - based on https://github.com/Phil-Factor/PSYaml + Creates a YAML description of the data in the object - based on https://github.com/Phil-Factor/PSYaml .DESCRIPTION - This produces YAML from any object you pass to it. + This function produces YAML from any object you pass to it. It supports objects received through the pipeline + and allows specifying the depth of the conversion. .PARAMETER InputObject The object that you want scripted out. This parameter accepts input via the pipeline. .PARAMETER Depth - The depth that you want your object scripted to + The depth to which you want your object scripted. .EXAMPLE - Get-PodeOpenApiDefinition|ConvertTo-PodeYaml + Get-PodeOpenApiDefinition | ConvertTo-PodeYaml #> function ConvertTo-PodeYaml { [CmdletBinding()] [OutputType([string])] param ( + # The object to be converted to YAML. Accepts pipeline input. [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] [AllowNull()] $InputObject, + # Specifies the depth to which the object should be converted to YAML. [parameter()] [int] $Depth = 16 ) begin { + # Initialize an array to store pipeline objects for later processing. $pipelineObject = @() - } + # Determine if the internal Pode YAML converter should be used. + $usePodeYamlInternal = $null -eq $PodeContext -or $PodeContext.Server.Web.OpenApi.UsePodeYamlInternal + + # Check if the YAML module has already been imported and cache the result. + $yamlModuleImported = $PodeContext.Server.InternalCache.YamlModuleImported + if ($null -eq $yamlModuleImported) { + # Test if either PSYaml or powershell-yaml module is installed. + $yamlModuleImported = ((Test-PodeModuleInstalled -Name 'PSYaml') -or (Test-PodeModuleInstalled -Name 'powershell-yaml')) + # Cache the result of the module check. + $PodeContext.Server.InternalCache.YamlModuleImported = $yamlModuleImported + } + } process { + # Add each pipeline object to the array for processing in the 'end' block. $pipelineObject += $_ } @@ -1508,21 +1532,21 @@ function ConvertTo-PodeYaml { $InputObject = $pipelineObject } - if ($null -eq $PodeContext -or $PodeContext.Server.Web.OpenApi.UsePodeYamlInternal) { - # return ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth -NoNewLine - return [Pode.PodeConverter]::ToYaml($InputObject, $Depth, 0, $true) - } - - if ($null -eq $PodeContext.Server.InternalCache.YamlModuleImported) { - $PodeContext.Server.InternalCache.YamlModuleImported = ((Test-PodeModuleInstalled -Name 'PSYaml') -or (Test-PodeModuleInstalled -Name 'powershell-yaml')) - } - - if ($PodeContext.Server.InternalCache.YamlModuleImported) { - return ($InputObject | ConvertTo-Yaml) + # Determine which YAML conversion method to use. + if ($usePodeYamlInternal -or -not $yamlModuleImported) { + # If using the internal Pode YAML converter and PowerShell Core, use the Pode.PodeConverter class. + if (Test-PodeIsPSCore) { + return [Pode.PodeConverter]::ToYaml($InputObject, $Depth, 0, $true) + } + else { + # Use the internal Pode YAML converter for Windows PowerShell. + return ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth -NoNewLine + } } else { - # return ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth -NoNewLine - return [Pode.PodeConverter]::ToYaml($InputObject, $Depth, 0, $true) + # If an external YAML module is available, use it for conversion. + return ($InputObject | ConvertTo-Yaml) } + } } \ No newline at end of file diff --git a/tests/unit/Helpers.Tests.ps1 b/tests/unit/Helpers.Tests.ps1 index 8187f034f..69c9ccd1d 100644 --- a/tests/unit/Helpers.Tests.ps1 +++ b/tests/unit/Helpers.Tests.ps1 @@ -1665,7 +1665,7 @@ Describe 'New-PodeCron' { } - + Describe 'ConvertTo-PodeYamlInternal Tests' { @@ -1683,7 +1683,7 @@ Describe 'ConvertTo-PodeYamlInternal Tests' { - two - three '@) - $result | Should -Be ($expected.Trim() -Replace "`r`n", "`n") + $result | Should -Be $expected.Trim() } It 'Converts hashtables correctly' { @@ -1692,7 +1692,7 @@ Describe 'ConvertTo-PodeYamlInternal Tests' { key2 = 'value2' } $result = ConvertTo-PodeYamlInternal -InputObject $hashTable -NoNewLine - $result | Should -Be "key1: value1`nkey2: value2" + $result | Should -Be "key1: value1$([Environment]::NewLine)key2: value2" } } @@ -1705,7 +1705,7 @@ Describe 'ConvertTo-PodeYamlInternal Tests' { } $result = ConvertTo-PodeYamlInternal -InputObject $nestedHash -NoNewLine - $result | Should -Be "parent: `n child: value" + $result | Should -Be "parent: $([Environment]::NewLine) child: value" } } diff --git a/tests/unit/Utilities.Tests.ps1 b/tests/unit/Utilities.Tests.ps1 index 88b80107b..ef88f0465 100644 --- a/tests/unit/Utilities.Tests.ps1 +++ b/tests/unit/Utilities.Tests.ps1 @@ -9,7 +9,24 @@ BeforeAll { Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' if (!([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Pode' })) { - Add-Type -LiteralPath "$($src)/Libs/netstandard2.0/Pode.dll" -ErrorAction Stop + $frameworkDescription = [System.Runtime.InteropServices.RuntimeInformation]::FrameworkDescription + $loaded = $false + if ($frameworkDescription -match '(\d+)\.(\d+)\.(\d+)') { + $majorVersion = [int]$matches[1] + + for ($version = $majorVersion; $version -ge 6; $version--) { + $dllPath = "$($src)/Libs/net$version.0/Pode.dll" + if (Test-Path $dllPath) { + Add-Type -LiteralPath $dllPath -ErrorAction Stop + $loaded = $true + break + } + } + } + + if (-not $loaded) { + Add-Type -LiteralPath "$($src)/Libs/netstandard2.0/Pode.dll" -ErrorAction Stop + } } function Compare-Hashtable { param ( @@ -118,12 +135,12 @@ Describe 'ConvertFrom-PodeYaml test' { BeforeAll { # Mocking the internal function ConvertFrom-PodeYamlInternal Mock -CommandName ConvertFrom-PodeYamlInternal -MockWith { - return @{ openapi = '3.0.3'; info = @{ title = 'Async test - OpenAPI 3.0'; version = '0.0.1' }; paths = @{ '/task/{taskId}' = @{ get = @{ summary = 'Get Pode Task Info' } } } } + return [ordered]@{ openapi = '3.0.3'; info = @{ title = 'Async test - OpenAPI 3.0'; version = '0.0.1' }; paths = @{ '/task/{taskId}' = @{ get = @{ summary = 'Get Pode Task Info' } } } } } # Mocking the external function ConvertFrom-Yaml Mock -CommandName ConvertFrom-Yaml -MockWith { - return @{ openapi = '3.0.3'; info = @{ title = 'Async test - OpenAPI 3.0'; version = '0.0.1' }; paths = @{ '/task/{taskId}' = @{ get = @{ summary = 'Get Pode Task Info' } } } } + return [ordered]@{ openapi = '3.0.3'; info = @{ title = 'Async test - OpenAPI 3.0'; version = '0.0.1' }; paths = @{ '/task/{taskId}' = @{ get = @{ summary = 'Get Pode Task Info' } } } } } # Mocking the Test-PodeModuleInstalled function @@ -161,7 +178,7 @@ Describe 'ConvertFrom-PodeYaml test' { # Assert-MockCalled -CommandName ConvertFrom-PodeYamlInternal -Times 1 Assert-MockCalled -CommandName ConvertFrom-Yaml -Times 0 - $result | Should -BeOfType 'ordered' + $result | Should -BeOfType 'System.Collections.Specialized.OrderedDictionary' $result.openapi | Should -Be '3.0.3' } } @@ -180,6 +197,7 @@ Describe 'ConvertFrom-PodeYaml test' { } } } + } It 'Should use the external converter if a YAML module is available' { @@ -198,7 +216,7 @@ Describe 'ConvertFrom-PodeYaml test' { Assert-MockCalled -CommandName ConvertFrom-Yaml -Times 1 # Assert-MockCalled -CommandName ConvertFrom-PodeYamlInternal -Times 0 - $result | Should -BeOfType 'hashtable' + $result | Should -BeOfType 'System.Collections.Specialized.OrderedDictionary' $result.openapi | Should -Be '3.0.3' } } From c96b1442160a3d14776f7b6f5b76f85038c86c23 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 7 Sep 2024 21:09:07 -0700 Subject: [PATCH 10/14] Update server.psd1 --- examples/server.psd1 | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/examples/server.psd1 b/examples/server.psd1 index aa26bdcd5..fe03207e7 100644 --- a/examples/server.psd1 +++ b/examples/server.psd1 @@ -72,17 +72,5 @@ Enable = $true } } - AsyncRoutes = @{ - HouseKeeping = @{ - TimerInterval = 30 - RetentionMinutes = 10 - } - } - Tasks = @{ - HouseKeeping = @{ - TimerInterval = 30 - RetentionMinutes = 1 - } - } } } \ No newline at end of file From 083feaa557bf29884ca2634a90d6e93237de965b Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 11 Sep 2024 08:32:09 -0700 Subject: [PATCH 11/14] add support for PodeOrderedConcurrentDictionary --- src/Listener/PodeConverter.cs | 11 +- .../PodeOrderedConcurrentDictionary.cs | 206 ++++++++++++++++++ 2 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 src/Listener/PodeOrderedConcurrentDictionary.cs diff --git a/src/Listener/PodeConverter.cs b/src/Listener/PodeConverter.cs index cf6a7f93c..f5a6278a1 100644 --- a/src/Listener/PodeConverter.cs +++ b/src/Listener/PodeConverter.cs @@ -2,6 +2,7 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Specialized; using System.Linq; using System.Management.Automation; @@ -34,7 +35,11 @@ public static string ToYaml(object inputObject, int depth = 10, int nestingLevel if (type != "String") { - if (inputObject is OrderedDictionary) + if (inputObject.GetType().GetGenericTypeDefinition() == typeof(ConcurrentDictionary<,>)) + { + type = "hashtable"; + } + else if (inputObject is OrderedDictionary) { type = "ordereddictionary"; } @@ -46,6 +51,10 @@ public static string ToYaml(object inputObject, int depth = 10, int nestingLevel { type = "hashtable"; } + else if (inputObject.GetType().IsGenericType && inputObject.GetType().GetGenericTypeDefinition() == typeof(PodeOrderedConcurrentDictionary<,>)) + { + type = "ordereddictionary"; + } } StringBuilder output = new StringBuilder(); diff --git a/src/Listener/PodeOrderedConcurrentDictionary.cs b/src/Listener/PodeOrderedConcurrentDictionary.cs new file mode 100644 index 000000000..623e0ebcc --- /dev/null +++ b/src/Listener/PodeOrderedConcurrentDictionary.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Pode +{ + public class PodeOrderedConcurrentDictionary + { + private readonly ConcurrentDictionary _concurrentDictionary; + private readonly SortedDictionary _sortedDictionary; + private readonly object _lock = new object(); + + // Constructor accepting a custom key comparer + public PodeOrderedConcurrentDictionary(IComparer keyComparer = null) + { + _concurrentDictionary = new ConcurrentDictionary(); + _sortedDictionary = new SortedDictionary(keyComparer ?? Comparer.Default); + } + + // Adds or updates an item in the dictionary + public void AddOrUpdate(TKey key, TValue value) + { + _concurrentDictionary[key] = value; // Thread-safe operation for the ConcurrentDictionary + + lock (_lock) // Lock to ensure the SortedDictionary remains thread-safe + { + _sortedDictionary[key] = value; + } + } + + // Adds or updates an item in the dictionary using value factories + public TValue AddOrUpdate(TKey key, Func addValueFactory, Func updateValueFactory) + { + TValue result = _concurrentDictionary.AddOrUpdate(key, addValueFactory, updateValueFactory); + + lock (_lock) + { + _sortedDictionary[key] = result; + } + + return result; + } + + // Attempts to add a key-value pair if the key does not already exist + public bool TryAdd(TKey key, TValue value) + { + // Try to add to the ConcurrentDictionary first + if (_concurrentDictionary.TryAdd(key, value)) + { + // If successful, add to the SortedDictionary inside a lock + lock (_lock) + { + _sortedDictionary[key] = value; + } + return true; + } + + return false; // If the key already exists, return false + } + + // Tries to get a value by key + public bool TryGetValue(TKey key, out TValue value) + { + return _concurrentDictionary.TryGetValue(key, out value); + } + + // Tries to remove a key-value pair + public bool TryRemove(TKey key, out TValue value) + { + var removed = _concurrentDictionary.TryRemove(key, out value); + + if (removed) + { + lock (_lock) + { + _sortedDictionary.Remove(key); + } + } + + return removed; + } + + // Attempts to update the value of the specified key if it matches a specified comparison value + public bool TryUpdate(TKey key, TValue newValue, TValue comparisonValue) + { + if (_concurrentDictionary.TryUpdate(key, newValue, comparisonValue)) + { + lock (_lock) + { + _sortedDictionary[key] = newValue; + } + return true; + } + + return false; + } + + // Gets or adds a value by key + public TValue GetOrAdd(TKey key, TValue value) + { + TValue result = _concurrentDictionary.GetOrAdd(key, value); + + lock (_lock) + { + if (!_sortedDictionary.ContainsKey(key)) + { + _sortedDictionary[key] = result; + } + } + + return result; + } + + public TValue GetOrAdd(TKey key, Func valueFactory) + { + TValue result = _concurrentDictionary.GetOrAdd(key, valueFactory); + + lock (_lock) + { + if (!_sortedDictionary.ContainsKey(key)) + { + _sortedDictionary[key] = result; + } + } + + return result; + } + + // Clears the dictionary + public void Clear() + { + _concurrentDictionary.Clear(); + lock (_lock) + { + _sortedDictionary.Clear(); + } + } + + // Checks if the dictionary contains the specified key + public bool ContainsKey(TKey key) + { + return _concurrentDictionary.ContainsKey(key); + } + + // Converts the dictionary to an array of key-value pairs + public KeyValuePair[] ToArray() + { + return _concurrentDictionary.ToArray(); + } + + // Indexer to support [] access in PowerShell + public TValue this[TKey key] + { + get + { + if (_concurrentDictionary.TryGetValue(key, out TValue value)) + { + return value; + } + throw new KeyNotFoundException($"The key '{key}' was not found in the dictionary."); + } + set + { + AddOrUpdate(key, value); + } + } + + // Returns ordered keys + public IEnumerable Keys + { + get + { + lock (_lock) + { + // Return a copy of the keys to maintain thread safety + return new List(_sortedDictionary.Keys); + } + } + } + + // Returns ordered values + public IEnumerable Values + { + get + { + lock (_lock) + { + // Return a copy of the values to maintain thread safety + return new List(_sortedDictionary.Values); + } + } + } + + // Returns the count of items in the dictionary + public int Count + { + get { return _concurrentDictionary.Count; } + } + + // Checks if the dictionary is empty + public bool IsEmpty + { + get { return _concurrentDictionary.IsEmpty; } + } + } +} \ No newline at end of file From f4cc4ddeddbada978f3d0107bcf1e802a6e9da0b Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 11 Sep 2024 08:57:22 -0700 Subject: [PATCH 12/14] fixes --- src/Listener/PodeConverter.cs | 13 ++++++++---- tests/unit/Utilities.Tests.ps1 | 36 +++++++++++++++++++--------------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/Listener/PodeConverter.cs b/src/Listener/PodeConverter.cs index f5a6278a1..cbdeff6c7 100644 --- a/src/Listener/PodeConverter.cs +++ b/src/Listener/PodeConverter.cs @@ -3,6 +3,7 @@ using System; using System.Collections; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Management.Automation; @@ -35,7 +36,7 @@ public static string ToYaml(object inputObject, int depth = 10, int nestingLevel if (type != "String") { - if (inputObject.GetType().GetGenericTypeDefinition() == typeof(ConcurrentDictionary<,>)) + if (inputObject.GetType().IsGenericType && inputObject.GetType().GetGenericTypeDefinition() == typeof(ConcurrentDictionary<,>)) { type = "hashtable"; } @@ -116,7 +117,11 @@ public static string ToYaml(object inputObject, int depth = 10, int nestingLevel } else { - if (stringValue.StartsWith("#") || stringValue.StartsWith("[") || stringValue.StartsWith("]") || stringValue.StartsWith("@") || stringValue.StartsWith("{") || stringValue.StartsWith("}") || stringValue.StartsWith("!") || stringValue.StartsWith("*")) + // Define the set of special characters that require wrapping in quotes + var specialChars = new HashSet { '#', '[', ']', '@', '{', '}', '!', '*' }; + + // Check if the first character is in the special characters set + if (stringValue.Length > 0 && specialChars.Contains(stringValue[0])) { output.AppendFormat("'{0}'", stringValue.Replace("'", "''")); } @@ -154,7 +159,7 @@ public static string ToYaml(object inputObject, int depth = 10, int nestingLevel stringBuilder.Append(ToYaml(item.Value, depth, nestingLevel + increment)); } } - output.Append(stringBuilder.ToString()); + output.Append(stringBuilder); } else { @@ -188,7 +193,7 @@ public static string ToYaml(object inputObject, int depth = 10, int nestingLevel stringBuilder.Append(ToYaml(item.Value, depth, nestingLevel + increment)); } } - output.Append(stringBuilder.ToString()); + output.Append(stringBuilder); } else { diff --git a/tests/unit/Utilities.Tests.ps1 b/tests/unit/Utilities.Tests.ps1 index ef88f0465..ab1836ab4 100644 --- a/tests/unit/Utilities.Tests.ps1 +++ b/tests/unit/Utilities.Tests.ps1 @@ -8,26 +8,30 @@ BeforeAll { $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' - if (!([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Pode' })) { - $frameworkDescription = [System.Runtime.InteropServices.RuntimeInformation]::FrameworkDescription - $loaded = $false - if ($frameworkDescription -match '(\d+)\.(\d+)\.(\d+)') { - $majorVersion = [int]$matches[1] - - for ($version = $majorVersion; $version -ge 6; $version--) { - $dllPath = "$($src)/Libs/net$version.0/Pode.dll" - if (Test-Path $dllPath) { - Add-Type -LiteralPath $dllPath -ErrorAction Stop - $loaded = $true - break - } - } + + $podeDll = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Pode' } + if (! $podeDll) { + # fetch the .net version and the libs path + $version = [System.Environment]::Version.Major + $libsPath = "$($src)/Libs" + + # filter .net dll folders based on version above, and get path for latest version found + if (![string]::IsNullOrWhiteSpace($version)) { + $netFolder = Get-ChildItem -Path $libsPath -Directory -Force | + Where-Object { $_.Name -imatch "net[1-$($version)]" } | + Sort-Object -Property Name -Descending | + Select-Object -First 1 -ExpandProperty FullName } - if (-not $loaded) { - Add-Type -LiteralPath "$($src)/Libs/netstandard2.0/Pode.dll" -ErrorAction Stop + # use netstandard if no folder found + if ([string]::IsNullOrWhiteSpace($netFolder)) { + $netFolder = "$($libsPath)/netstandard2.0" } + + # append Pode.dll and mount + Add-Type -LiteralPath "$($netFolder)/Pode.dll" -ErrorAction Stop } + function Compare-Hashtable { param ( [hashtable]$Hashtable1, From 7456ec2ed62a54746737b5582bc1c1a1d138d498 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 21 Sep 2024 14:37:47 -0700 Subject: [PATCH 13/14] Update Schedules.Tests.ps1 --- tests/integration/Schedules.Tests.ps1 | 47 +++++++++------------------ 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/tests/integration/Schedules.Tests.ps1 b/tests/integration/Schedules.Tests.ps1 index 74a53bd14..ab4e1f898 100644 --- a/tests/integration/Schedules.Tests.ps1 +++ b/tests/integration/Schedules.Tests.ps1 @@ -23,16 +23,14 @@ Describe 'Schedules' { Set-PodeState -Name 'test3' -Value @{eventList = @() } - Add-PodeSchedule -Name 'TestEvents' -Cron '* * * * *' -Limit 2 -ScriptBlock { - param($Event, $Message1, $Message2) + Add-PodeSchedule -Name 'TestEvents' -Cron '* * * * *' -Limit 2 -OnStart -ScriptBlock { + param($Event ) Lock-PodeObject -ScriptBlock { $test3 = (Get-PodeState -Name 'test3') $test3.eventList += @{ - message = 'Hello, world!' - 'Last' = $Event.Sender.LastTriggerTime - 'Next' = $Event.Sender.NextTriggerTime - 'Message1' = $Message1 - 'Message2' = $Message2 + message = 'Hello, world!' + 'Last' = $Event.Sender.LastTriggerTime + 'Next' = $Event.Sender.NextTriggerTime } } } @@ -50,16 +48,6 @@ Describe 'Schedules' { } } - - # adhoc invoke a schedule's logic - Add-PodeRoute -Method Post -Path '/eventlist/run' -ScriptBlock { - Invoke-PodeSchedule -Name 'TestEvents' -ArgumentList @{ - Message1 = 'Hello!' - Message2 = 'Bye!' - } - Write-PodeJsonResponse -Value ( @{Result = 'ok' } ) - } - # test1 Set-PodeState -Name 'Test1' -Value 0 Add-PodeSchedule -Name 'Test1' -Cron '* * * * *' -ScriptBlock { @@ -95,10 +83,6 @@ Describe 'Schedules' { Get-Job -Name 'Pode' | Remove-Job -Force } - It 'Invoke schedule events' { - $result = Invoke-RestMethod -Uri "$($Endpoint)/eventlist/run" -Method post - $result.Result | Should -Be 'OK' - } It 'Schedule updates state value - full cron' { $result = Invoke-RestMethod -Uri "$($Endpoint)/test1" -Method Get $result.Result | Should -Be 1337 @@ -122,17 +106,16 @@ Describe 'Schedules' { $result.Count | Should -Be 2 $result.eventList.GetType() | Should -Be 'System.Object[]' $result.eventList.Count | Should -Be 2 - $result.eventList[0].Message1 | Should -Be 'Hello!' - $result.eventList[0].Message2 | Should -Be 'Bye!' - $result.eventList[0].Message | Should -Be 'Hello, world!' - $result.eventList[0].Last | Should -BeNullOrEmpty - $result.eventList[0].next | Should -not -BeNullOrEmpty - - $result.eventList[1].Message1 | Should -BeNullOrEmpty - $result.eventList[1].Message2 | Should -BeNullOrEmpty - $result.eventList[1].Message | Should -Be 'Hello, world!' - $result.eventList[1].Last | Should -not -BeNullOrEmpty - $result.eventList[1].next | Should -not -BeNullOrEmpty + + + if ( $null -eq $result.eventList[0].Next ) { $index = 0 } else { $index = 1 } + $result.eventList[$index].Message | Should -Be 'Hello, world!' + $result.eventList[$index].Last | Should -not -BeNullOrEmpty + $result.eventList[$index].next | Should -BeNullOrEmpty + if ($index -eq 0) { $index = 1 }else { $index = 0 } + $result.eventList[$index].Message | Should -Be 'Hello, world!' + $result.eventList[$index].Last | Should -not -BeNullOrEmpty + $result.eventList[$index].next | Should -not -BeNullOrEmpty } } \ No newline at end of file From 04f280d0812b78623c72fef7c3cf6e181aac9be0 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 22 Nov 2024 06:24:58 -0800 Subject: [PATCH 14/14] Update pode.build.ps1 --- pode.build.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/pode.build.ps1 b/pode.build.ps1 index d82731d5c..d195004e7 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -113,6 +113,7 @@ function Invoke-PodeBuildDotnetBuild($target) { # Determine if the target framework is compatible $isCompatible = $False switch ($majorVersion) { + 9 { if ($target -in @('net6.0', 'netstandard2.0', 'net8.0', 'net9.0')) { $isCompatible = $True } } 8 { if ($target -in @('net6.0', 'netstandard2.0', 'net8.0')) { $isCompatible = $True } } 7 { if ($target -in @('net6.0', 'netstandard2.0')) { $isCompatible = $True } } 6 { if ($target -in @('net6.0', 'netstandard2.0')) { $isCompatible = $True } }