diff --git a/docs/Tutorials/Configuration.md b/docs/Tutorials/Configuration.md
index f6a8720fa..42992a322 100644
--- a/docs/Tutorials/Configuration.md
+++ b/docs/Tutorials/Configuration.md
@@ -101,3 +101,6 @@ A "path" like `Server.Ssl.Protocols` looks like the below in the file:
| 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) |
+| 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)| |
+
diff --git a/examples/server.psd1 b/examples/server.psd1
index ec5b15edf..42f4ea69b 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
diff --git a/src/Listener/Pode.csproj b/src/Listener/Pode.csproj
index f481af38e..f106b194e 100644
--- a/src/Listener/Pode.csproj
+++ b/src/Listener/Pode.csproj
@@ -20,4 +20,14 @@
$(TargetFrameworks);net10.0
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Listener/PodeConverter.cs b/src/Listener/PodeConverter.cs
new file mode 100644
index 000000000..cbdeff6c7
--- /dev/null
+++ b/src/Listener/PodeConverter.cs
@@ -0,0 +1,336 @@
+#if !NETSTANDARD2_0
+
+using System;
+using System.Collections;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+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.GetType().IsGenericType && inputObject.GetType().GetGenericTypeDefinition() == typeof(ConcurrentDictionary<,>))
+ {
+ type = "hashtable";
+ }
+ else if (inputObject is OrderedDictionary)
+ {
+ type = "ordereddictionary";
+ }
+ else if (inputObject is IList)
+ {
+ type = "array";
+ }
+ else if (inputObject is Hashtable)
+ {
+ type = "hashtable";
+ }
+ else if (inputObject.GetType().IsGenericType && inputObject.GetType().GetGenericTypeDefinition() == typeof(PodeOrderedConcurrentDictionary<,>))
+ {
+ type = "ordereddictionary";
+ }
+ }
+
+ 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
+ {
+ // 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("'", "''"));
+ }
+ 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);
+ }
+ 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);
+ }
+ 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;
+ }
+ }
+
+
+
+}
+#endif
\ No newline at end of file
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
diff --git a/src/Pode.psd1 b/src/Pode.psd1
index aff2bfcb7..48b357e9e 100644
--- a/src/Pode.psd1
+++ b/src/Pode.psd1
@@ -141,6 +141,8 @@
'ConvertFrom-PodeXml',
'Set-PodeDefaultFolder',
'Get-PodeDefaultFolder',
+ 'ConvertTo-PodeYaml',
+ 'ConvertFrom-PodeYaml',
'Get-PodeCurrentRunspaceName',
'Set-PodeCurrentRunspaceName',
'Invoke-PodeGC',
diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1
index 19d4c1004..c373b7e40 100644
--- a/src/Private/Context.ps1
+++ b/src/Private/Context.ps1
@@ -1126,6 +1126,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 0b8e8d425..a18900144 100644
--- a/src/Private/Helpers.ps1
+++ b/src/Private/Helpers.ps1
@@ -1419,10 +1419,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 )
}
}
@@ -1556,10 +1556,15 @@ 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)
+ }
}
}
@@ -1567,6 +1572,10 @@ function ConvertFrom-PodeRequestContent {
$Result.Data = [xml]($Content)
}
+ { $_ -ilike '*/yaml' } {
+ $Result.Data = ($Content | ConvertFrom-PodeYaml -AsHashtable:$PodeContext.Server.Web.Conversion.YamlToHashTable)
+ }
+
{ $_ -ilike '*/csv' } {
$Result.Data = ($Content | ConvertFrom-Csv)
}
@@ -3386,66 +3395,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.
@@ -3544,7 +3493,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++) {
@@ -3607,7 +3556,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]) {
@@ -3633,7 +3582,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]) {
@@ -3658,7 +3607,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()
@@ -3987,6 +3936,165 @@ function Test-PodeIsISEHost {
return ((Test-PodeIsWindows) -and ('Windows PowerShell ISE Host' -eq $Host.Name))
}
+
+
+
+<#
+.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
+ }
+}
+
+
<#
.SYNOPSIS
Determines the MIME type of an image from its binary header.
diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1
index e193bbbc3..25ab5c8ff 100644
--- a/src/Public/Utilities.ps1
+++ b/src/Public/Utilities.ps1
@@ -1611,3 +1611,184 @@ function Start-PodeSleep {
+
+<#
+.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.
+
+.PARAMETER AsHashTable
+ Converts the YAML to a hash table object
+
+.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([System.Collections.Specialized.OrderedDictionary])]
+ param (
+ [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
+ [AllowNull()]
+ [string[]]
+ $InputObject,
+
+ [Parameter()]
+ [switch]
+ $AsHashTable
+ )
+
+ 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 += $_
+ }
+
+ end {
+ # If multiple objects were passed through the pipeline, join them into a single string.
+ if ( $InputObject.Count -gt 1) {
+ $obj = $InputObject -join [Environment]::NewLine
+ }
+ else {
+ $obj = $InputObject[0]
+ }
+
+ # 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 {
+ # Use the internal Pode YAML converter for Windows PowerShell.
+ $result = ConvertFrom-PodeYamlInternal -InputObject $obj
+ }
+ }
+ 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
+
+ }
+}
+
+<#
+.SYNOPSIS
+ Creates a YAML description of the data in the object - based on https://github.com/Phil-Factor/PSYaml
+
+.DESCRIPTION
+ 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 to which you want your object scripted.
+
+.EXAMPLE
+ 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 += $_
+ }
+
+ end {
+ if ($pipelineObject.Count -gt 1) {
+ $InputObject = $pipelineObject
+ }
+
+ # 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 {
+ # If an external YAML module is available, use it for conversion.
+ return ($InputObject | ConvertTo-Yaml)
+ }
+
+ }
+}
+
diff --git a/tests/unit/Helpers.Tests.ps1 b/tests/unit/Helpers.Tests.ps1
index 54a298804..2a2a4d214 100644
--- a/tests/unit/Helpers.Tests.ps1
+++ b/tests/unit/Helpers.Tests.ps1
@@ -1664,7 +1664,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' {
@@ -1673,7 +1673,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"
}
}
@@ -1686,7 +1686,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
new file mode 100644
index 000000000..ab1836ab4
--- /dev/null
+++ b/tests/unit/Utilities.Tests.ps1
@@ -0,0 +1,324 @@
+[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'
+
+ $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
+ }
+
+ # 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,
+ [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
+ }
+
+
+ # 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' {
+ 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$([Environment]::NewLine)key2: value2"
+ }
+ }
+
+ Context 'When converting complex objects' {
+ It 'Handles nested hashtables' {
+ $nestedHash = @{
+ parent = @{
+ child = 'value'
+ }
+ }
+ $result = $nestedHash | ConvertTo-PodeYaml
+
+ $result | Should -Be "parent: $([Environment]::NewLine) 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 test' {
+
+ Context 'YAML Module' {
+ BeforeAll {
+ # Mocking the internal function ConvertFrom-PodeYamlInternal
+ Mock -CommandName ConvertFrom-PodeYamlInternal -MockWith {
+ 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 [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
+ 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 'System.Collections.Specialized.OrderedDictionary'
+ $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 'System.Collections.Specialized.OrderedDictionary'
+ $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 {
+ $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'
+ }
+ }
+
+}