From fba1c4882c7367aa74690aa0dcd0b16529502318 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 7 Sep 2024 17:58:58 -0700 Subject: [PATCH 1/8] recover from Add Functions for Handling Route WebEvent Data and Serialization in Pode #1353 --- docs/Tutorials/Routes/Overview.md | 91 +- docs/Tutorials/Routes/Parameters/Body.md | 100 +++ docs/Tutorials/Routes/Parameters/Cookies.md | 94 ++ docs/Tutorials/Routes/Parameters/Headers.md | 91 ++ docs/Tutorials/Routes/Parameters/Paths.md | 93 ++ docs/Tutorials/Routes/Parameters/Queries.md | 89 ++ src/Pode.psd1 | 7 +- src/Public/Cookies.ps1 | 99 ++- src/Public/Headers.ps1 | 76 +- src/Public/Utilities.ps1 | 934 ++++++++++++++++++++ tests/unit/Utility.Tests.ps1 | 596 +++++++++++++ 11 files changed, 2140 insertions(+), 130 deletions(-) create mode 100644 docs/Tutorials/Routes/Parameters/Body.md create mode 100644 docs/Tutorials/Routes/Parameters/Cookies.md create mode 100644 docs/Tutorials/Routes/Parameters/Headers.md create mode 100644 docs/Tutorials/Routes/Parameters/Paths.md create mode 100644 docs/Tutorials/Routes/Parameters/Queries.md create mode 100644 tests/unit/Utility.Tests.ps1 diff --git a/docs/Tutorials/Routes/Overview.md b/docs/Tutorials/Routes/Overview.md index a9acd7efe..aba0f2b6d 100644 --- a/docs/Tutorials/Routes/Overview.md +++ b/docs/Tutorials/Routes/Overview.md @@ -39,96 +39,21 @@ The scriptblock for the route will have access to the `$WebEvent` variable which You can add your routes straight into the [`Start-PodeServer`](../../../Functions/Core/Start-PodeServer) scriptblock, or separate them into different files. These files can then be dot-sourced, or you can use [`Use-PodeRoutes`](../../../Functions/Routes/Use-PodeRoutes) to automatically load all ps1 files within a `/routes` directory at the root of your server. -## Payloads +## Retrieving Client Parameters -The following is an example of using data from a request's payload - ie, the data in the body of POST request. To retrieve values from the payload you can use the `.Data` property on the `$WebEvent` variable to a route's logic. +When working with REST calls, data can be passed by the client using various methods, including Cookies, Headers, Paths, Queries, and Body. Each of these methods has specific ways to retrieve the data: -Depending the the Content-Type supplied, Pode has inbuilt body-parsing logic for JSON, XML, CSV, and Form data. +- **Cookies**: Cookies sent by the client can be accessed using the `$WebEvent.Cookies` property or the `Get-PodeCookie` function for more advanced handling. For more details, refer to the [Cookies Documentation](./Parameters/Cookies.md). -This example will get the `userId` and "find" user, returning the users data: +- **Headers**: Headers can be retrieved using the `$WebEvent.Request.Headers` property or the `Get-PodeHeader` function, which provides additional deserialization options. Learn more in the [Headers Documentation](./Parameters/Headers.md). -```powershell -Start-PodeServer { - Add-PodeEndpoint -Address * -Port 8080 -Protocol Http +- **Paths**: Parameters passed through the URL path can be accessed using the `$WebEvent.Parameters` property or the `Get-PodePathParameter` function. Detailed information can be found in the [Path Parameters Documentation](./Parameters/Paths.md). - Add-PodeRoute -Method Post -Path '/users' -ScriptBlock { - # get the user - $user = Get-DummyUser -UserId $WebEvent.Data.userId - - # return the user - Write-PodeJsonResponse -Value @{ - Username = $user.username - Age = $user.age - } - } -} -``` - -The following request will invoke the above route: - -```powershell -Invoke-WebRequest -Uri 'http://localhost:8080/users' -Method Post -Body '{ "userId": 12345 }' -ContentType 'application/json' -``` +- **Queries**: Query parameters from the URL can be accessed via `$WebEvent.Query` or retrieved using the `Get-PodeQueryParameter` function for deserialization support. Check the [Query Parameters Documentation](./Parameters/Queries.md). -!!! important - The `ContentType` is required as it informs Pode on how to parse the requests payload. For example, if the content type were `application/json`, then Pode will attempt to parse the body of the request as JSON - converting it to a hashtable. +- **Body**: Data sent in the request body, such as in POST requests, can be retrieved using the `$WebEvent.Data` property or the `Get-PodeBodyData` function for enhanced deserialization capabilities. See the [Body Data Documentation](./Parameters/Body.md) for more information. -!!! important - On PowerShell 5 referencing JSON data on `$WebEvent.Data` must be done as `$WebEvent.Data.userId`. This also works in PowerShell 6+, but you can also use `$WebEvent.Data['userId']` on PowerShell 6+. - -## Query Strings - -The following is an example of using data from a request's query string. To retrieve values from the query string you can use the `.Query` property from the `$WebEvent` variable. This example will return a user based on the `userId` supplied: - -```powershell -Start-PodeServer { - Add-PodeEndpoint -Address * -Port 8080 -Protocol Http - - Add-PodeRoute -Method Get -Path '/users' -ScriptBlock { - # get the user - $user = Get-DummyUser -UserId $WebEvent.Query['userId'] - - # return the user - Write-PodeJsonResponse -Value @{ - Username = $user.username - Age = $user.age - } - } -} -``` - -The following request will invoke the above route: - -```powershell -Invoke-WebRequest -Uri 'http://localhost:8080/users?userId=12345' -Method Get -``` - -## Parameters - -The following is an example of using values supplied on a request's URL using parameters. To retrieve values that match a request's URL parameters you can use the `.Parameters` property from the `$WebEvent` variable. This example will get the `:userId` and "find" user, returning the users data: - -```powershell -Start-PodeServer { - Add-PodeEndpoint -Address * -Port 8080 -Protocol Http - - Add-PodeRoute -Method Get -Path '/users/:userId' -ScriptBlock { - # get the user - $user = Get-DummyUser -UserId $WebEvent.Parameters['userId'] - - # return the user - Write-PodeJsonResponse -Value @{ - Username = $user.username - Age = $user.age - } - } -} -``` - -The following request will invoke the above route: - -```powershell -Invoke-WebRequest -Uri 'http://localhost:8080/users/12345' -Method Get -``` +Each link provides detailed usage and examples to help you retrieve and manipulate the parameters passed by the client effectively. ## Script from File diff --git a/docs/Tutorials/Routes/Parameters/Body.md b/docs/Tutorials/Routes/Parameters/Body.md new file mode 100644 index 000000000..3cf33a0f4 --- /dev/null +++ b/docs/Tutorials/Routes/Parameters/Body.md @@ -0,0 +1,100 @@ +# Body Payloads + +The following is an example of using data from a request's payload—i.e., the data in the body of a POST request. To retrieve values from the payload, you can use the `.Data` property on the `$WebEvent` variable in a route's logic. + +Alternatively, you can use the `Get-PodeBodyData` function to retrieve the body data, with additional support for deserialization. + +Depending on the Content-Type supplied, Pode has built-in body-parsing logic for JSON, XML, CSV, and Form data. + +This example will get the `userId` and "find" the user, returning the user's data: + +```powershell +Start-PodeServer { + Add-PodeEndpoint -Address * -Port 8080 -Protocol Http + + Add-PodeRoute -Method Post -Path '/users' -ScriptBlock { + # get the user + $user = Get-DummyUser -UserId $WebEvent.Data.userId + + # return the user + Write-PodeJsonResponse -Value @{ + Username = $user.username + Age = $user.age + } + } +} +``` + +The following request will invoke the above route: + +```powershell +Invoke-WebRequest -Uri 'http://localhost:8080/users' -Method Post -Body '{ "userId": 12345 }' -ContentType 'application/json' +``` + +!!! important + The `ContentType` is required as it informs Pode on how to parse the request's payload. For example, if the content type is `application/json`, Pode will attempt to parse the body of the request as JSON—converting it to a hashtable. + +!!! important + On PowerShell 5, referencing JSON data on `$WebEvent.Data` must be done as `$WebEvent.Data.userId`. This also works in PowerShell 6+, but you can also use `$WebEvent.Data['userId']` on PowerShell 6+. + +### Using Get-PodeBodyData + +Alternatively, you can use the `Get-PodeBodyData` function to retrieve the body data. This function works similarly to the `.Data` property on `$WebEvent` and supports the same content types. + +Here is the same example using `Get-PodeBodyData`: + +```powershell +Start-PodeServer { + Add-PodeEndpoint -Address * -Port 8080 -Protocol Http + + Add-PodeRoute -Method Post -Path '/users' -ScriptBlock { + # get the body data + $body = Get-PodeBodyData + + # get the user + $user = Get-DummyUser -UserId $body.userId + + # return the user + Write-PodeJsonResponse -Value @{ + Username = $user.username + Age = $user.age + } + } +} +``` + +### Deserialization with Get-PodeBodyData + +The `Get-PodeBodyData` function can also deserialize body data from requests, allowing for more complex data handling scenarios. This feature is especially useful when dealing with serialized data structures that require specific interpretation styles. + +To enable deserialization, use the `-Deserialize` switch along with the following options: + +- **`-NoExplode`**: Prevents deserialization from exploding arrays in the body data. This is useful when dealing with comma-separated values where array expansion is not desired. +- **`-Style`**: Defines the deserialization style (`'Simple'`, `'Label'`, `'Matrix'`, `'Form'`, `'SpaceDelimited'`, `'PipeDelimited'`, `'DeepObject'`) to interpret the body data correctly. The default style is `'Form'`. +- **`-KeyName`**: Specifies the key name to use when deserializing, allowing accurate mapping of the body data. The default value for `KeyName` is `'id'`. + +### Example with Deserialization + +This example demonstrates deserialization of body data using specific styles and options: + +```powershell +Start-PodeServer { + Add-PodeEndpoint -Address * -Port 8080 -Protocol Http + + Add-PodeRoute -Method Post -Path '/items' -ScriptBlock { + # retrieve and deserialize the body data + $body = Get-PodeBodyData -Deserialize -Style 'Matrix' -NoExplode + + # get the item based on the deserialized data + $item = Get-DummyItem -ItemId $body.id + + # return the item details + Write-PodeJsonResponse -Value @{ + Name = $item.name + Quantity = $item.quantity + } + } +} +``` + +In this example, `Get-PodeBodyData` is used to deserialize the body data with the `'Matrix'` style and prevent array explosion (`-NoExplode`). This approach provides flexible and precise handling of incoming body data, enhancing the capability of your Pode routes to manage complex payloads. \ No newline at end of file diff --git a/docs/Tutorials/Routes/Parameters/Cookies.md b/docs/Tutorials/Routes/Parameters/Cookies.md new file mode 100644 index 000000000..60acf7445 --- /dev/null +++ b/docs/Tutorials/Routes/Parameters/Cookies.md @@ -0,0 +1,94 @@ + +# Cookies + +The following is an example of using values supplied in a request's cookies. To retrieve values from the cookies, you can use the `Cookies` property from the `$WebEvent` variable. + +Alternatively, you can use the `Get-PodeCookie` function to retrieve the cookie data, with additional support for deserialization and secure handling. + +This example will get the `SessionId` cookie and use it to authenticate the user, returning a success message: + +```powershell +Start-PodeServer { + Add-PodeEndpoint -Address * -Port 8080 -Protocol Http + + Add-PodeRoute -Method Get -Path '/authenticate' -ScriptBlock { + # get the session ID from the cookie + $sessionId = $WebEvent.Cookies['SessionId'] + + # authenticate the session + $isAuthenticated = Authenticate-Session -SessionId $sessionId + + # return the result + Write-PodeJsonResponse -Value @{ + Authenticated = $isAuthenticated + } + } +} +``` + +The following request will invoke the above route: + +```powershell +Invoke-WebRequest -Uri 'http://localhost:8080/authenticate' -Method Get -Headers @{ Cookie = 'SessionId=abc123' } +``` + +## Using Get-PodeCookie + +Alternatively, you can use the `Get-PodeCookie` function to retrieve the cookie data. This function works similarly to the `Cookies` property on `$WebEvent`, but it provides additional options for deserialization and secure cookie handling. + +Here is the same example using `Get-PodeCookie`: + +```powershell +Start-PodeServer { + Add-PodeEndpoint -Address * -Port 8080 -Protocol Http + + Add-PodeRoute -Method Get -Path '/authenticate' -ScriptBlock { + # get the session ID from the cookie + $sessionId = Get-PodeCookie -Name 'SessionId' + + # authenticate the session + $isAuthenticated = Authenticate-Session -SessionId $sessionId + + # return the result + Write-PodeJsonResponse -Value @{ + Authenticated = $isAuthenticated + } + } +} +``` + +### Deserialization with Get-PodeCookie + +The `Get-PodeCookie` function can also deserialize cookie values, allowing for more complex handling of serialized data sent in cookies. This feature is particularly useful when cookies contain encoded or structured content that needs specific parsing. + +To enable deserialization, use the `-Deserialize` switch along with the following options: + +- **`-NoExplode`**: Prevents deserialization from exploding arrays in the cookie value. This is useful when handling comma-separated values where array expansion is not desired. +- **`-Deserialize`**: Indicates that the retrieved cookie value should be deserialized, interpreting the content based on the provided deserialization style and options. + +### Example with Deserialization + +This example demonstrates deserialization of a cookie value: + +```powershell +Start-PodeServer { + Add-PodeEndpoint -Address * -Port 8080 -Protocol Http + + Add-PodeRoute -Method Get -Path '/deserialize-cookie' -ScriptBlock { + # retrieve and deserialize the 'Session' cookie + $sessionData = Get-PodeCookie -Name 'Session' -Deserialize -NoExplode + + # process the deserialized cookie data + # (example processing logic here) + + # return the processed cookie data + Write-PodeJsonResponse -Value @{ + SessionData = $sessionData + } + } +} +``` + +In this example, `Get-PodeCookie` is used to deserialize the `Session` cookie, interpreting it according to the provided deserialization options. The `-NoExplode` switch ensures that any arrays within the cookie value are not expanded during deserialization. + +For further information on general usage and retrieving cookies, please refer to the [Headers Documentation](Cookies.md). \ No newline at end of file diff --git a/docs/Tutorials/Routes/Parameters/Headers.md b/docs/Tutorials/Routes/Parameters/Headers.md new file mode 100644 index 000000000..46ff05fa9 --- /dev/null +++ b/docs/Tutorials/Routes/Parameters/Headers.md @@ -0,0 +1,91 @@ +# Headers + +The following is an example of using values supplied in a request's headers. To retrieve values from the headers, you can use the `Headers` property from the `$WebEvent.Request` variable. Alternatively, you can use the `Get-PodeHeader` function to retrieve the header data. + +This example will get the Authorization header and validate the token, returning a success message: + +```powershell +Start-PodeServer { + Add-PodeEndpoint -Address * -Port 8080 -Protocol Http + + Add-PodeRoute -Method Get -Path '/validate' -ScriptBlock { + # get the token + $token = $WebEvent.Request.Headers['Authorization'] + + # validate the token + $isValid = Test-PodeJwt -payload $token + + # return the result + Write-PodeJsonResponse -Value @{ + Success = $isValid + } + } +} +``` + +The following request will invoke the above route: + +```powershell +Invoke-WebRequest -Uri 'http://localhost:8080/validate' -Method Get -Headers @{ Authorization = 'Bearer some_token' } +``` + +## Using Get-PodeHeader + +Alternatively, you can use the `Get-PodeHeader` function to retrieve the header data. This function works similarly to the `Headers` property on `$WebEvent.Request`. + +Here is the same example using `Get-PodeHeader`: + +```powershell +Start-PodeServer { + Add-PodeEndpoint -Address * -Port 8080 -Protocol Http + + Add-PodeRoute -Method Get -Path '/validate' -ScriptBlock { + # get the token + $token = Get-PodeHeader -Name 'Authorization' + + # validate the token + $isValid = Test-PodeJwt -payload $token + + # return the result + Write-PodeJsonResponse -Value @{ + Success = $isValid + } + } +} +``` + +### Deserialization with Get-PodeHeader + +The `Get-PodeHeader` function can also deserialize header values, enabling more advanced handling of serialized data sent in headers. This feature is useful when dealing with complex data structures or when headers contain encoded or serialized content. + +To enable deserialization, use the `-Deserialize` switch along with the following options: + +- **`-Explode`**: Specifies whether the deserialization process should explode arrays in the header value. This is useful when handling comma-separated values within the header. +- **`-Deserialize`**: Indicates that the retrieved header value should be deserialized, interpreting the content based on the deserialization style and options. + +### Example with Deserialization + +This example demonstrates deserialization of a header value: + +```powershell +Start-PodeServer { + Add-PodeEndpoint -Address * -Port 8080 -Protocol Http + + Add-PodeRoute -Method Get -Path '/deserialize' -ScriptBlock { + # retrieve and deserialize the 'X-SerializedHeader' header + $headerData = Get-PodeHeader -Name 'X-SerializedHeader' -Deserialize -Explode + + # process the deserialized header data + # (example processing logic here) + + # return the processed header data + Write-PodeJsonResponse -Value @{ + HeaderData = $headerData + } + } +} +``` + +In this example, `Get-PodeHeader` is used to deserialize the `X-SerializedHeader` header, interpreting it according to the provided deserialization options. The `-Explode` switch ensures that any arrays within the header value are properly expanded during deserialization. + +For further information on general usage and retrieving headers, please refer to the [Headers Documentation](Headers.md). diff --git a/docs/Tutorials/Routes/Parameters/Paths.md b/docs/Tutorials/Routes/Parameters/Paths.md new file mode 100644 index 000000000..f26bbba87 --- /dev/null +++ b/docs/Tutorials/Routes/Parameters/Paths.md @@ -0,0 +1,93 @@ + +# Paths + +The following is an example of using values supplied on a request's URL using parameters. To retrieve values that match a request's URL parameters, you can use the `Parameters` property from the `$WebEvent` variable. + +Alternatively, you can use the `Get-PodePathParameter` function to retrieve the parameter data. + +This example will get the `:userId` and "find" user, returning the user's data: + +```powershell +Start-PodeServer { + Add-PodeEndpoint -Address * -Port 8080 -Protocol Http + + Add-PodeRoute -Method Get -Path '/users/:userId' -ScriptBlock { + # get the user + $user = Get-DummyUser -UserId $WebEvent.Parameters['userId'] + + # return the user + Write-PodeJsonResponse -Value @{ + Username = $user.username + Age = $user.age + } + } +} +``` + +The following request will invoke the above route: + +```powershell +Invoke-WebRequest -Uri 'http://localhost:8080/users/12345' -Method Get +``` + +### Using Get-PodePathParameter + +Alternatively, you can use the `Get-PodePathParameter` function to retrieve the parameter data. This function works similarly to the `Parameters` property on `$WebEvent` but provides additional options for deserialization when needed. + +Here is the same example using `Get-PodePathParameter`: + +```powershell +Start-PodeServer { + Add-PodeEndpoint -Address * -Port 8080 -Protocol Http + + Add-PodeRoute -Method Get -Path '/users/:userId' -ScriptBlock { + # get the parameter data + $userId = Get-PodePathParameter -Name 'userId' + + # get the user + $user = Get-DummyUser -UserId $userId + + # return the user + Write-PodeJsonResponse -Value @{ + Username = $user.username + Age = $user.age + } + } +} +``` + +#### Deserialization with Get-PodePathParameter + +The `Get-PodePathParameter` function can handle deserialization of parameters passed in the URL path, query string, or body, using specific styles to interpret the data correctly. This is useful when dealing with more complex data structures or encoded parameter values. + +To enable deserialization, use the `-Deserialize` switch along with the following options: + +- **`-Explode`**: Specifies whether to explode arrays when deserializing, useful when parameters contain comma-separated values. +- **`-Style`**: Defines the deserialization style (`'Simple'`, `'Label'`, or `'Matrix'`) to interpret the parameter value correctly. The default style is `'Simple'`. +- **`-KeyName`**: Specifies the key name to use when deserializing, allowing you to map the parameter data accurately. The default value for `KeyName` is `'id'`. + +#### Example with Deserialization + +This example demonstrates deserialization of a parameter that is styled and exploded as part of the request: + +```powershell +Start-PodeServer { + Add-PodeEndpoint -Address * -Port 8080 -Protocol Http + + Add-PodeRoute -Method Get -Path '/items/:itemId' -ScriptBlock { + # retrieve and deserialize the 'itemId' parameter + $itemId = Get-PodePathParameter -Name 'itemId' -Deserialize -Style 'Label' -Explode + + # get the item based on the deserialized data + $item = Get-DummyItem -ItemId $itemId + + # return the item details + Write-PodeJsonResponse -Value @{ + Name = $item.name + Quantity = $item.quantity + } + } +} +``` + +In this example, the `Get-PodePathParameter` function is used to deserialize the `itemId` parameter, interpreting it according to the specified style (`Label`) and handling arrays if present (`-Explode`). The default `KeyName` is `'id'`, but it can be customized as needed. This approach allows for dynamic and precise handling of incoming request data, making your Pode routes more versatile and resilient. \ No newline at end of file diff --git a/docs/Tutorials/Routes/Parameters/Queries.md b/docs/Tutorials/Routes/Parameters/Queries.md new file mode 100644 index 000000000..6b9b99245 --- /dev/null +++ b/docs/Tutorials/Routes/Parameters/Queries.md @@ -0,0 +1,89 @@ +# Queries + +The following is an example of using data from a request's query string. To retrieve values from the query parameters, you can use the `Query` property on the `$WebEvent` variable in a route's logic. + +Alternatively, you can use the `Get-PodeQueryParameter` function to retrieve the query parameter data, with additional support for deserialization. + +This example will return a user based on the `userId` supplied: + +```powershell +Start-PodeServer { + Add-PodeEndpoint -Address * -Port 8080 -Protocol Http + + Add-PodeRoute -Method Get -Path '/users' -ScriptBlock { + # get the user + $user = Get-DummyUser -UserId $WebEvent.Query['userId'] + + # return the user + Write-PodeJsonResponse -Value @{ + Username = $user.username + Age = $user.age + } + } +} +``` + +The following request will invoke the above route: + +```powershell +Invoke-WebRequest -Uri 'http://localhost:8080/users?userId=12345' -Method Get +``` + +### Using Get-PodeQueryParameter + +Alternatively, you can use the `Get-PodeQueryParameter` function to retrieve the query data. This function works similarly to the `Query` property on `$WebEvent` but provides additional options for deserialization when needed. + +Here is the same example using `Get-PodeQueryParameter`: + +```powershell +Start-PodeServer { + Add-PodeEndpoint -Address * -Port 8080 -Protocol Http + + Add-PodeRoute -Method Get -Path '/users' -ScriptBlock { + # get the query data + $userId = Get-PodeQueryParameter -Name 'userId' + + # get the user + $user = Get-DummyUser -UserId $userId + + # return the user + Write-PodeJsonResponse -Value @{ + Username = $user.username + Age = $user.age + } + } +} +``` + +#### Deserialization with Get-PodeQueryParameter + +The `Get-PodeQueryParameter` function can also deserialize query parameters passed in the URL, using specific styles to interpret the data correctly. This feature is particularly useful when handling complex data structures or encoded parameter values. + +To enable deserialization, use the `-Deserialize` switch along with the following options: + +- **`-NoExplode`**: Prevents deserialization from exploding arrays when handling comma-separated values. This is useful when array expansion is not desired. +- **`-Style`**: Defines the deserialization style (`'Simple'`, `'Label'`, `'Matrix'`, `'Form'`, `'SpaceDelimited'`, `'PipeDelimited'`, `'DeepObject'`) to interpret the query parameter value correctly. The default style is `'Form'`. +- **`-KeyName`**: Specifies the key name to use when deserializing, allowing you to map the query parameter data accurately. The default value for `KeyName` is `'id'`. + +#### Example with Deserialization + +This example demonstrates deserialization of a query parameter with specific styles and options: + +```powershell +Start-PodeServer { + Add-PodeEndpoint -Address * -Port 8080 -Protocol Http + + Add-PodeRoute -Method Get -Path '/items' -ScriptBlock { + # retrieve and deserialize the 'filter' query parameter + $filter = Get-PodeQueryParameter -Name 'filter' -Deserialize -Style 'SpaceDelimited' -NoExplode + + # get items based on the deserialized filter data + $items = Get-DummyItems -Filter $filter + + # return the item details + Write-PodeJsonResponse -Value $items + } +} +``` + +In this example, the `Get-PodeQueryParameter` function is used to deserialize the `filter` query parameter, interpreting it according to the specified style (`SpaceDelimited`) and preventing array explosion (`-NoExplode`). This approach allows for dynamic and precise handling of complex query data, enhancing the flexibility of your Pode routes. \ No newline at end of file diff --git a/src/Pode.psd1 b/src/Pode.psd1 index eae7d8c44..86a3c0cad 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -141,7 +141,12 @@ 'ConvertFrom-PodeXml', 'Set-PodeDefaultFolder', 'Get-PodeDefaultFolder', - + 'Get-PodeBodyData', + 'Get-PodeQueryParameter', + 'Get-PodePathParameter', + 'ConvertFrom-PodeSerializedString', + 'ConvertTo-PodeSerializedString', + # routes 'Add-PodeRoute', 'Add-PodeStaticRoute', diff --git a/src/Public/Cookies.ps1 b/src/Public/Cookies.ps1 index 6bff3e2a0..65245f41d 100644 --- a/src/Public/Cookies.ps1 +++ b/src/Public/Cookies.ps1 @@ -109,69 +109,112 @@ function Set-PodeCookie { <# .SYNOPSIS -Retrieves a cookie from the Request. + Retrieves a specified cookie from the incoming request. .DESCRIPTION -Retrieves a cookie from the Request, with the option to supply a secret to unsign the cookie's value. + The `Get-PodeCookie` function retrieves a cookie from the incoming request. + It can unsign the cookie's value using a specified secret, which can be extended with the client request's UserAgent and RemoteIPAddress if `-Strict` is specified. The function also allows for returning the raw .NET Cookie object for direct manipulation or deserializing serialized cookie values for more complex handling. .PARAMETER Name -The name of the cookie to retrieve. + The name of the cookie to retrieve. This parameter is mandatory. .PARAMETER Secret -The secret used to unsign the cookie's value. + The secret used to unsign the cookie's value, ensuring the integrity and authenticity of the cookie data. + Applicable only in the 'BuiltIn' parameter set. .PARAMETER Strict -If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress. + If specified, the secret is extended using the client's UserAgent and RemoteIPAddress, adding an extra layer of + security when unsigning the cookie. Applicable only in the 'BuiltIn' parameter set. .PARAMETER Raw -If supplied, the cookie returned will be the raw .NET Cookie object for manipulation. + If specified, the cookie returned will be the raw .NET Cookie object, allowing for direct manipulation of + the cookie. This is useful for scenarios where the full cookie object is needed. Applicable only in the 'BuiltIn' parameter set. + +.PARAMETER Deserialize + Indicates that the retrieved cookie value should be deserialized. When this switch is used, the value will be + interpreted based on the deserialization options provided. This parameter is mandatory in the 'Deserialize' parameter set. + +.PARAMETER NoExplode + Prevents deserialization from exploding arrays in the cookie value, which is useful when handling comma-separated + values without expanding them into arrays. Applicable only when the `-Deserialize` switch is used. + +.EXAMPLE + Get-PodeCookie -Name 'Views' + Retrieves the value of the 'Views' cookie from the request. .EXAMPLE -Get-PodeCookie -Name 'Views' + Get-PodeCookie -Name 'Views' -Secret 'hunter2' + Retrieves and unsigns the 'Views' cookie using the specified secret. .EXAMPLE -Get-PodeCookie -Name 'Views' -Secret 'hunter2' + Get-PodeCookie -Name 'Session' -Deserialize -NoExplode + Retrieves and deserializes the 'Session' cookie value without exploding arrays. + +.EXAMPLE + Get-PodeCookie -Name 'AuthToken' -Raw + Retrieves the raw .NET Cookie object for the 'AuthToken' cookie, allowing for direct manipulation. + +.NOTES + This function should be used within a route's script block in a Pode server. The `-Deserialize` switch provides + advanced handling of serialized cookie values, while the `-Secret` and `-Strict` options offer secure methods for + unsigning cookies. #> function Get-PodeCookie { - [CmdletBinding()] - [OutputType([hashtable])] + [CmdletBinding(DefaultParameterSetName = 'BuiltIn' )] param( - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $true, ParameterSetName = 'Deserialize')] + [Parameter(Mandatory = $true, ParameterSetName = 'BuiltIn')] [string] $Name, - [Parameter()] + [Parameter(ParameterSetName = 'BuiltIn')] [string] $Secret, + [Parameter(ParameterSetName = 'BuiltIn')] [switch] $Strict, + [Parameter(ParameterSetName = 'BuiltIn')] [switch] - $Raw + $Raw, + + [Parameter(ParameterSetName = 'Deserialize')] + [switch] + $NoExplode, + + [Parameter(Mandatory = $true, ParameterSetName = 'Deserialize')] + [switch] + $Deserialize ) + if ($WebEvent) { + # get the cookie from the request + $cookie = $WebEvent.Cookies[$Name] + if (!$Raw) { + $cookie = (ConvertTo-PodeCookie -Cookie $cookie) + } - # get the cookie from the request - $cookie = $WebEvent.Cookies[$Name] - if (!$Raw) { - $cookie = (ConvertTo-PodeCookie -Cookie $cookie) - } + if (($null -eq $cookie) -or [string]::IsNullOrWhiteSpace($cookie.Value)) { + return $null + } - if (($null -eq $cookie) -or [string]::IsNullOrWhiteSpace($cookie.Value)) { - return $null - } + if ($Deserialize.IsPresent) { + $cookie.Value = ConvertFrom-PodeSerializedString -SerializedString $cookie.Value -Style 'Form' -Explode:(!$NoExplode) + } - # if a secret was supplied, attempt to unsign the cookie - if (![string]::IsNullOrWhiteSpace($Secret)) { - $value = (Invoke-PodeValueUnsign -Value $cookie.Value -Secret $Secret -Strict:$Strict) - if (![string]::IsNullOrWhiteSpace($value)) { - $cookie.Value = $value + # if a secret was supplied, attempt to unsign the cookie + if (![string]::IsNullOrWhiteSpace($Secret)) { + $value = (Invoke-PodeValueUnsign -Value $cookie.Value -Secret $Secret -Strict:$Strict) + if (![string]::IsNullOrWhiteSpace($value)) { + $cookie.Value = $value + } } - } - return $cookie + return $cookie + } } + <# .SYNOPSIS Retrieves the value of a cookie from the Request. diff --git a/src/Public/Headers.ps1 b/src/Public/Headers.ps1 index 84a907b2f..9b8db7173 100644 --- a/src/Public/Headers.ps1 +++ b/src/Public/Headers.ps1 @@ -133,48 +133,88 @@ function Test-PodeHeader { <# .SYNOPSIS -Retrieves the value of a header from the Request. + Retrieves the value of a specified header from the incoming request. .DESCRIPTION -Retrieves the value of a header from the Request. + The `Get-PodeHeader` function retrieves the value of a specified header from the incoming request. + It supports deserialization of header values and can optionally unsign the header using a specified secret. + The unsigning process can be further secured with the client's UserAgent and RemoteIPAddress if `-Strict` is specified. .PARAMETER Name -The name of the header to retrieve. + The name of the header to retrieve. This parameter is mandatory. .PARAMETER Secret -The secret used to unsign the header's value. + The secret used to unsign the header's value. This option is useful when working with signed headers to ensure + the integrity and authenticity of the value. Applicable only in the 'BuiltIn' parameter set. .PARAMETER Strict -If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress. + If specified, the secret is extended using the client's UserAgent and RemoteIPAddress, providing an additional + layer of security during the unsigning process. Applicable only in the 'BuiltIn' parameter set. + +.PARAMETER Deserialize + Indicates that the retrieved header value should be deserialized. When this switch is used, the value will be + interpreted based on the provided deserialization options. This parameter is mandatory in the 'Deserialize' parameter set. + +.PARAMETER Explode + Specifies whether the deserialization process should explode arrays in the header value. This is useful when + handling comma-separated values within the header. Applicable only when the `-Deserialize` switch is used. + +.EXAMPLE + Get-PodeHeader -Name 'X-AuthToken' + Retrieves the value of the 'X-AuthToken' header from the request. .EXAMPLE -Get-PodeHeader -Name 'X-AuthToken' + Get-PodeHeader -Name 'X-SerializedHeader' -Deserialize -Explode + Retrieves and deserializes the value of the 'X-SerializedHeader' header, exploding arrays if present. + +.EXAMPLE + Get-PodeHeader -Name 'X-AuthToken' -Secret 'MySecret' -Strict + Retrieves and unsigns the 'X-AuthToken' header using the specified secret, extending it with UserAgent and + RemoteIPAddress information for added security. + +.NOTES + This function should be used within a route's script block in a Pode server. The `-Deserialize` switch enables + advanced handling of serialized header values, while the `-Secret` and `-Strict` options provide secure unsigning + capabilities for signed headers. #> function Get-PodeHeader { - [CmdletBinding()] - [OutputType([string])] + [CmdletBinding(DefaultParameterSetName = 'BuiltIn' )] param( - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $true, ParameterSetName = 'Deserialize')] + [Parameter(Mandatory = $true, ParameterSetName = 'BuiltIn')] [string] $Name, - [Parameter()] + [Parameter(ParameterSetName = 'BuiltIn')] [string] $Secret, + [Parameter(ParameterSetName = 'BuiltIn')] [switch] - $Strict + $Strict, + + [Parameter(ParameterSetName = 'Deserialize')] + [switch] + $Explode, + + [Parameter(Mandatory = $true, ParameterSetName = 'Deserialize')] + [switch] + $Deserialize ) + if ($WebEvent) { + # get the value for the header from the request + $header = $WebEvent.Request.Headers.$Name - # get the value for the header from the request - $header = $WebEvent.Request.Headers.$Name + if ($Deserialize.IsPresent) { + return ConvertFrom-PodeSerializedString -SerializedString $header -Style 'Simple' -Explode:$Explode + } + # if a secret was supplied, attempt to unsign the header's value + if (![string]::IsNullOrWhiteSpace($Secret)) { + $header = (Invoke-PodeValueUnsign -Value $header -Secret $Secret -Strict:$Strict) + } - # if a secret was supplied, attempt to unsign the header's value - if (![string]::IsNullOrWhiteSpace($Secret)) { - $header = (Invoke-PodeValueUnsign -Value $header -Secret $Secret -Strict:$Strict) + return $header } - - return $header } <# diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index dde5ff1bb..ceefd03ef 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -1364,4 +1364,938 @@ function ConvertFrom-PodeXml { } return $oHash +} + +<# +.SYNOPSIS + Converts a hashtable to a serialized string using a specified serialization style. + +.DESCRIPTION + The ConvertTo-PodeSerializedString function takes a hashtable and converts it to a serialized string + according to the specified serialization style. It supports various serialization styles such as + 'Simple', 'Label', 'Matrix', 'Query', 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. + An optional 'Explode' switch can be used to modify the serialization format for certain styles. + +.PARAMETER Hashtable + The hashtable to be serialized. + +.PARAMETER Style + The style of serialization to be used. Valid values are 'Simple', 'Label', 'Matrix', 'Query', + 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. + +.PARAMETER Explode + An optional switch to modify the serialization format for certain styles. + +.EXAMPLE + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + } + $serialized = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Query' + Write-Output $serialized + +.EXAMPLE + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + } + $serializedExplode = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'DeepObject' -Explode + Write-Output $serializedExplode +#> +function ConvertTo-PodeSerializedString { + + param ( + [Parameter(Mandatory, ValueFromPipeline = $true, Position = 0)] + [hashtable[]] + $Hashtable, + + [Parameter()] + [ValidateSet('Simple', 'Label', 'Matrix', 'Query', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' )] + [string] + $Style = 'Simple', + + [Parameter()] + [switch] + $Explode + ) + begin { + $pipelineValue = @() + } + + process { + $pipelineValue += $_ + } + + end { + if ($pipelineValue.Count -gt 1) { + $Hashtables = $pipelineValue + } + else { + $Hashtables = $Hashtable + } + $serializedArray = @() + foreach ( $Hashtable in $Hashtables) { + switch ($Style) { + 'Simple' { + if ($Explode) { + $serializedArray += ($Hashtable.Keys | ForEach-Object { "$($_)=$($Hashtable.""$_"")" }) -join '&' + } + else { + $serializedArray += ($Hashtable.Keys | ForEach-Object { "$($_)=$($Hashtable.""$_"")" }) -join ',' + } + } + 'Label' { + $serializedArray += ($Hashtable.Keys | ForEach-Object { ".$_.$($Hashtable.""$_"")" }) -join '' + } + 'Matrix' { + $serializedArray += ($Hashtable.Keys | ForEach-Object { ";$_=$($Hashtable.""$_"")" }) -join '' + } + 'Query' { + $serializedArray += ($Hashtable.Keys | ForEach-Object { "$($_)=$($Hashtable.""$_"")" }) -join '&' + } + 'Form' { + $serializedArray += ($Hashtable.Keys | ForEach-Object { "$($_)=$($Hashtable.""$_"")" }) -join '&' + } + 'SpaceDelimited' { + $serializedArray += ($Hashtable.Keys | ForEach-Object { "$($_)=$($Hashtable.""$_"")" }) -join ' ' + } + 'PipeDelimited' { + $serializedArray += ($Hashtable.Keys | ForEach-Object { "$($_)=$($Hashtable.""$_"")" }) -join '|' + } + 'DeepObject' { + if ($Explode) { + $serializedArray += ($Hashtable.Keys | ForEach-Object { "$($_)[$($_)]=$($Hashtable.""$_"")" }) -join '&' + } + else { + $serializedArray += ($Hashtable.Keys | ForEach-Object { "$($_)[$($_)]=$($Hashtable.""$_"")" }) -join ',' + } + } + } + } + return $serializedArray -join '&' + } +} + +<# +.SYNOPSIS + Converts a serialized string back into a hashtable, automatically detecting the serialization style. + +.DESCRIPTION + The ConvertFrom-PodeSerializedString function takes a serialized string and converts it back into a hashtable. + The function automatically detects the serialization style based on common delimiters and formats, such as + 'Simple', 'Label', 'Matrix', 'Query', 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. + If the serialized string starts with a variable name followed by a colon (e.g., X-MyHeader:), the function + will encapsulate the result within a hashtable under that variable name. + +.PARAMETER SerializedString + The serialized string to be converted back into a hashtable. + +.PARAMETER Style + The serialization style to use for deserialization. Options are 'Simple', 'Label', 'Matrix', 'Query', 'Form', + 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. + +.PARAMETER Explode + Indicates whether the string uses exploded serialization (true) or not (false). This affects how arrays and objects are handled. + +.PARAMETER KeyName + Specifies the key name to match when processing certain styles, such as 'Matrix' and 'DeepObject'. + +.EXAMPLE + # Simple style, explode = true + $serialized = "name=value,anotherName=anotherValue" + $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'Simple' -Explode + Write-Output $hashtable + +.EXAMPLE + # Simple style, explode = false + $serialized = "name,value,anotherName,anotherValue" + $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'Simple' + Write-Output $hashtable + +.EXAMPLE + # Label style, explode = true + $serialized = ".name.value.anotherName.anotherValue" + $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'Label' -Explode + Write-Output $hashtable + +.EXAMPLE + # Label style, explode = false + $serialized = ".name,value,anotherName,anotherValue" + $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'Label' + Write-Output $hashtable + +.EXAMPLE + # Matrix style, explode = true + $serialized = ";name=value;anotherName=anotherValue" + $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'Matrix' -Explode + Write-Output $hashtable + +.EXAMPLE + # Matrix style, explode = false + $serialized = ";id=3;id=4;id=5" + $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'Matrix' + Write-Output $hashtable + +.EXAMPLE + # Query style, explode = true + $serialized = "name=value&anotherName=anotherValue" + $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'Query' -Explode + Write-Output $hashtable + +.EXAMPLE + # Query style, explode = false + $serialized = "name=value,anotherName=anotherValue" + $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'Query' + Write-Output $hashtable + +.EXAMPLE + # Form style, explode = true + $serialized = "name=value&anotherName=anotherValue" + $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'Form' -Explode + Write-Output $hashtable + +.EXAMPLE + # Form style, explode = false + $serialized = "name=value,anotherName=anotherValue" + $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'Form' + Write-Output $hashtable + +.EXAMPLE + # SpaceDelimited style, explode = true + $serialized = "id=3&id=4&id=5" + $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'SpaceDelimited' -Explode + Write-Output $hashtable + +.EXAMPLE + # SpaceDelimited style, explode = false + $serialized = "id=3%204%205" + $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'SpaceDelimited' + Write-Output $hashtable + +.EXAMPLE + # PipeDelimited style, explode = true + $serialized = "id=3&id=4&id=5" + $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'PipeDelimited' -Explode + Write-Output $hashtable + +.EXAMPLE + # PipeDelimited style, explode = false + $serialized = "id=3|4|5" + $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'PipeDelimited' + Write-Output $hashtable + +.EXAMPLE + # DeepObject style, explode = true + $serialized = "myId[role]=admin&myId[firstName]=Alex" + $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'DeepObject' -Explode -KeyName 'myId' + Write-Output $hashtable +#> +function ConvertFrom-PodeSerializedString { + param ( + [Parameter(Mandatory, ValueFromPipeline = $true, Position = 0)] + [string] $SerializedString, + + [Parameter()] + [ValidateSet('Simple', 'Label', 'Matrix', 'Query', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' )] + [string] + $Style = 'Form', + + [Parameter()] + [switch] + $Explode, + + [Parameter()] + [string] + $KeyName = 'id' # Default key name if not specified + ) + + process { + $SerializedString = $SerializedString.Replace('%20', ' ') + # Main deserialization logic based on style + switch ($Style) { + 'Simple' { + # Check for header pattern and extract it if present + if ($SerializedString -match '^([a-zA-Z0-9_-]+):') { + # Extract the variable name and strip it from the serialized string + $headerName = $matches[1] + $SerializedString = ($SerializedString -replace "^$($headerName):", '').Trim() + } + + $segments = $SerializedString -split ',' + + # If there's only one segment, return it directly + if ($segments.Count -eq 1) { + $result = $segments[0] + } + else { + if ($Explode) { + # Handling explode=true case + + # Check if the number of '=' is equal to the count of segments + if ((($SerializedString -split '=').Count - 1) -eq $segments.Count) { + $obj = @{} + foreach ($pair in $segments) { + if ($pair.Contains('=')) { + $key, $value = $pair -split '=', 2 # Split into exactly two parts + $obj[$key] = $value + } + } + $result = $obj + } + else { + # Return as an array if the explode conditions don't match + $result = $segments + } + } + else { + # Handling explode=false case + + # Check if it's likely an object by checking if the count of segments is even + if ($segments.Count % 2 -eq 0) { + # Try to parse as an object + $obj = @{} + for ($i = 0; $i -lt $segments.Count; $i += 2) { + $key = $segments[$i] + # Validate the key format + if ($key -match '^[a-zA-Z_][a-zA-Z0-9_]*$') { + $obj[$key] = $segments[$i + 1] + } + else { + # If the key is invalid, return the original segments as an array + $result = $segments + } + } + # Return the object if all keys are valid + $result = $obj + } + else { + # If not an object, treat it as an array + $result = $segments + } + } + } + + if ($headerName) { + return @{$headerName = $result } + } + else { + $result + } + + } + 'Label' { + # Remove the leading dot (.) prefix from the serialized string + $SerializedString = $SerializedString.TrimStart('.') + + # Split the string by dot + $segments = $SerializedString -split '\.' + + # Handle the explode=true case + if ($Explode) { + # Handling explode=true: each segment is a key=value pair + $obj = @{} + foreach ($segment in $segments) { + if ($segment.Contains('=')) { + $key, $value = $segment -split '=', 2 # Split into exactly two parts + $obj[$key] = $value + } + else { + # If a segment does not contain '=', treat it as an array element + return $segments -split ',' + } + } + return $obj + } + else { + # Handling explode=false: all segments form a combined structure + # Split the string by commas within each segment + $combinedSegments = ($SerializedString -split ',') + + # Check if it's likely an object by checking if the count is even + if ($combinedSegments.Count % 2 -eq 0) { + # Try to parse as an object + $obj = @{} + for ($i = 0; $i -lt $combinedSegments.Count; $i += 2) { + $key = $combinedSegments[$i] + + # Validate if the key is a suitable key + if ($key -match '^[a-zA-Z_][a-zA-Z0-9_]*$') { + $value = $combinedSegments[$i + 1] + $obj[$key] = $value + } + else { + # If validation fails, return segments as array + return $combinedSegments + } + } + return $obj + } + + # If not an object, return as an array + return $combinedSegments + } + } + 'Matrix' { + # Handle the explode=true case + if ($Explode) { + # Remove the leading semicolon (;) prefix from the serialized string + $SerializedString = $SerializedString.TrimStart(';') + + # Split by semicolon to get segments + $segments = $SerializedString -split ';' + + # If each segment doesn't contain '=', treat it as an array + if ($segments -notmatch '=') { + # Return as an array of individual elements split by commas + return $segments -split ',' + } + + # Initialize an empty hashtable to store key-value pairs + $obj = @{} + $values = @() + + + foreach ($segment in $segments) { + if ($segment.Contains('=')) { + $key, $value = $segment -split '=', 2 + + # If the key matches the specified key name + if ($key -eq $KeyName) { + $values += $value + } + else { + # If a key doesn't match, treat as a normal key-value pair in the hashtable + $obj[$key] = $value + } + } + } + + # If all segments matched the specified key name, return the values as an array + if ($values.Count -eq $segments.Count) { + if ($values.Count -eq 1) { + return $values[0] + } + return $values + } + + # Merge values back into the object if any key matches the KeyName + if ($values.Count -gt 0) { + $obj[$KeyName] = if ($values.Count -eq 1) { $values[0] } else { $values } + } + + # Return the hashtable if it contains any key-value pairs + if ($obj.Count -gt 0) { + return $obj + } + else { + return $values + } + } + else { + # Handling explode=false: + + # Remove the leading semicolon (;) prefix from the serialized string + $SerializedString = $SerializedString.TrimStart(";$KeyName=") + + # Split by semicolon to get segments + $segments = $SerializedString -split ',' + + # If there's only one segment, return it directly + if ($segments.Count -eq 1) { + return $segments[0] + } + + # Check if it's likely an object by checking if the count of segments is even + if ($segments.Count % 2 -eq 0) { + # Try to parse as an object + $obj = @{} + for ($i = 0; $i -lt $segments.Count; $i += 2) { + $key = $segments[$i] + # Validate the key format + if ($key -match '^[a-zA-Z_][a-zA-Z0-9_]*$') { + $obj[$key] = $segments[$i + 1] + } + else { + # If the key is invalid, return the original segments as an array + return $segments + } + } + # Return the object if all keys are valid + return $obj + } + + # If not an object, treat it as an array + return $segments + } + } + + 'Form' { + # Check for header pattern and extract it if present + if ($SerializedString -match '^([a-zA-Z0-9_-]+):') { + # Extract the variable name and strip it from the serialized string + $headerName = $matches[1] + $SerializedString = ($SerializedString -replace "^$($headerName):", '').Trim().TrimStart("$KeyName=") + } + else { + if ($Explode) { + # Remove the leading semicolon (;) prefix from the serialized string + $SerializedString = $SerializedString.TrimStart('?') + } + else { + # Remove the leading semicolon (;) prefix from the serialized string + $SerializedString = $SerializedString.TrimStart("?$KeyName=") + } + } + + # Handle the explode=true case + if ($Explode) { + # Split by semicolon to get segments + $segments = $SerializedString -split '&' + + # If each segment doesn't contain '=', treat it as an array + if ($segments -notmatch '=') { + # Return as an array of individual elements split by commas + $result = $segments -split ',' + } + else { + # Initialize an empty hashtable to store key-value pairs + $obj = @{} + $values = @() + + foreach ($segment in $segments) { + if ($segment.Contains('=')) { + $key, $value = $segment -split '=', 2 + + # If the key matches the specified key name + if ($key -eq $KeyName) { + $values += $value + } + else { + # If a key doesn't match, treat as a normal key-value pair in the hashtable + $obj[$key] = $value + } + } + } + + # If all segments matched the specified key name, return the values as an array + if ($values.Count -eq $segments.Count) { + if ($values.Count -eq 1) { + $result = $values[0] + } + else { + $result = $values + } + } + else { + + # Merge values back into the object if any key matches the KeyName + if ($values.Count -gt 0) { + $obj[$KeyName] = if ($values.Count -eq 1) { $values[0] } else { $values } + } + + # Return the hashtable if it contains any key-value pairs + if ($obj.Count -gt 0) { + return $obj + } + else { + return $values + } + } + } + } + else { + # Handling explode=false + + # Split by semicolon to get segments + $segments = $SerializedString -split ',' + + # If there's only one segment, return it directly + if ($segments.Count -eq 1) { + $result = $segments[0] + } + # Check if it's likely an object by checking if the count of segments is even + elseif ($segments.Count % 2 -eq 0) { + # Try to parse as an object + $obj = @{} + for ($i = 0; $i -lt $segments.Count; $i += 2) { + $key = $segments[$i] + # Validate the key format + if ($key -match '^[a-zA-Z_][a-zA-Z0-9_]*$') { + $obj[$key] = $segments[$i + 1] + } + else { + # If the key is invalid, return the original segments as an array + $result = $segments + break + } + } + if (!$result) { + # Return the object if all keys are valid + $result = $obj + } + } + else { + # If not an object, treat it as an array + $result = $segments + } + + } + + if ($headerName) { + return @{$headerName = $result } + } + else { + $result + } + } + + 'SpaceDelimited' { + if ($Explode) { + # Remove the leading semicolon (;) prefix from the serialized string + $SerializedString = $SerializedString.TrimStart('?') + + # For explode=true, split by '&' to treat each value as a separate occurrence + $segments = $SerializedString -split '&' + + # Initialize an array to store values that match the specified KeyName + $values = @() + foreach ($segment in $segments) { + if ($segment.Contains('=')) { + $key, $value = $segment -split '=', 2 + # Only add values where the key matches the specified KeyName + if ($key -eq $KeyName) { + $values += $value + } + } + } + # Return the array of values that matched the KeyName + return $values + } + else { + # Remove the leading semicolon (;) prefix from the serialized string + $SerializedString = $SerializedString.TrimStart('?id=') + # For explode=false, split by space (%20) to handle the combined string format + return $SerializedString -split ' ' + } + } + + 'PipeDelimited' { + if ($Explode) { + $SerializedString = $SerializedString.TrimStart('?') + # For explode=true, split by '&' to treat each value as a separate occurrence + $segments = $SerializedString -split '&' + + # Initialize an array to store values that match the specified KeyName + $values = @() + foreach ($segment in $segments) { + if ($segment.Contains('=')) { + $key, $value = $segment -split '=', 2 + # Only add values where the key matches the specified KeyName + if ($key -eq $KeyName) { + $values += $value + } + } + } + # Return the array of values that matched the KeyName + return $values + } + else { + # Remove the leading semicolon (;) prefix from the serialized string + $SerializedString = $SerializedString.TrimStart('?id=') + # For explode=false, split by space (%20) to handle the combined string format + return $SerializedString -split '\|' + } + } + + 'DeepObject' { + $SerializedString = $SerializedString.TrimStart('?') + + # Split the string by '&' to get each key-value pair + $segments = $SerializedString -split '&' + + # Initialize an empty hashtable to store the nested key-value pairs + $obj = @{} + foreach ($segment in $segments) { + if ($segment.Contains('=')) { + # Split each segment by '=' into key and value + $key, $value = $segment -split '=', 2 + + # Extract the main key and nested keys using regex + $allMatches = [regex]::Matches($key, '([^\[\]]+)') + + # Extract the main key (first match) and remaining nested keys + $mainKey = $allMatches[0].Groups[1].Value + # Manually extract remaining nested keys as a list of strings + $nestedKeys = @() + for ($i = 1; $i -lt $allMatches.Count; $i++) { + $nestedKeys += $allMatches[$i].Groups[1].Value + } + + # Only process the segment if the main key matches the specified KeyName + if ($mainKey -eq $KeyName) { + # Initialize a reference to the root object + $current = $obj + + # Iterate over the nested keys to build the structure + foreach ($nestedKey in $nestedKeys) { + # If this is the last key, assign the value + if ($nestedKey -eq $nestedKeys[-1]) { + $current[$nestedKey] = $value + } + else { + # Create a new hashtable if the nested key doesn't exist + if (-not $current.ContainsKey($nestedKey)) { + $current[$nestedKey] = @{} + } + # Move deeper into the nested structure + $current = $current[$nestedKey] + } + } + } + } + } + + # Return the constructed hashtable with nested keys and values + return $obj + } + + } + } +} + +<# +.SYNOPSIS + Retrieves a specific parameter value from the current Pode web event. + +.DESCRIPTION + The `Get-PodePathParameter` function extracts and returns the value of a specified parameter + from the current Pode web event. This function can access parameters passed in the URL path, query string, + or body of a web request, making it useful in web applications to dynamically handle incoming data. + + The function supports deserialization of parameter values when the `-Deserialize` switch is used. + This allows for interpreting serialized data structures, like arrays or complex objects, from the web request. + +.PARAMETER Name + The name of the parameter to retrieve. This parameter is mandatory. + +.PARAMETER Deserialize + Specifies that the parameter value should be deserialized. When this switch is used, the value will be interpreted + based on the provided style and other deserialization options. + +.PARAMETER Explode + Specifies whether to explode arrays when deserializing the parameter value. This is useful when parameters contain + comma-separated values. Applicable only when the `-Deserialize` switch is used. + +.PARAMETER Style + Defines the deserialization style to use when interpreting the parameter value. Valid options are 'Simple', 'Label', + and 'Matrix'. The default is 'Simple'. Applicable only when the `-Deserialize` switch is used. + +.PARAMETER KeyName + Specifies the key name to use when deserializing the parameter value. The default value is 'id'. + This option is useful for mapping the parameter data accurately during deserialization. Applicable only + when the `-Deserialize` switch is used. + +.EXAMPLE + Get-PodePathParameter -Name 'action' + Returns the value of the 'action' parameter from the current web event. + +.EXAMPLE + Get-PodePathParameter -Name 'item' -Deserialize -Style 'Label' -Explode + Retrieves and deserializes the value of the 'item' parameter using the 'Label' style and exploding arrays. + +.EXAMPLE + Get-PodePathParameter -Name 'id' -Deserialize -KeyName 'userId' + Deserializes the 'id' parameter using the key name 'userId'. + +.NOTES + This function should be used within a route's script block in a Pode server. + The `-Deserialize` switch enables more advanced handling of complex data structures. +#> +function Get-PodePathParameter { + [CmdletBinding(DefaultParameterSetName = 'BuiltIn' )] + param( + [Parameter(Mandatory, ParameterSetName = 'Deserialize')] + [Parameter(Mandatory, ParameterSetName = 'BuiltIn')] + [string] + $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'Deserialize')] + [switch] + $Deserialize, + + [Parameter(ParameterSetName = 'Deserialize')] + [switch] + $Explode, + + [Parameter(ParameterSetName = 'Deserialize')] + [ValidateSet('Simple', 'Label', 'Matrix')] + [string] + $Style = 'Simple', + + [Parameter(ParameterSetName = 'Deserialize')] + [string] + $KeyName = 'id' + + ) + if ($WebEvent) { + if ($Deserialize.IsPresent) { + return ConvertFrom-PodeSerializedString -SerializedString $WebEvent.Parameters[$Name] -Style $Style -Explode:$Explode -KeyName $KeyName + } + return $WebEvent.Parameters[$Name] + } +} + + +<# +.SYNOPSIS + Retrieves a specific query parameter value from the current Pode web event. + +.DESCRIPTION + The `Get-PodeQueryParameter` function extracts and returns the value of a specified query parameter + from the current Pode web event. This function is designed to access query parameters passed in the URL of a web request, + enabling the handling of incoming data in web applications. + + The function supports deserialization of query parameter values when the `-Deserialize` switch is used, + allowing for interpretation of complex data structures from the query string. + +.PARAMETER Name + The name of the query parameter to retrieve. This parameter is mandatory. + +.PARAMETER Deserialize + Specifies that the query parameter value should be deserialized. When this switch is used, the value will be + interpreted based on the provided style and other deserialization options. + +.PARAMETER NoExplode + Prevents deserialization from exploding arrays in the query parameter value. This is useful when handling + parameters that contain comma-separated values and when array expansion is not desired. Applicable only when + the `-Deserialize` switch is used. + +.PARAMETER Style + Defines the deserialization style to use when interpreting the query parameter value. Valid options are 'Simple', + 'Label', 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. The default is 'Form'. + Applicable only when the `-Deserialize` switch is used. + +.PARAMETER KeyName + Specifies the key name to use when deserializing the query parameter value. The default value is 'id'. + This option is useful for mapping the query parameter data accurately during deserialization. Applicable only + when the `-Deserialize` switch is used. + +.EXAMPLE + Get-PodeQueryParameter -Name 'userId' + Returns the value of the 'userId' query parameter from the current web event. + +.EXAMPLE + Get-PodeQueryParameter -Name 'filter' -Deserialize -Style 'SpaceDelimited' + Retrieves and deserializes the value of the 'filter' query parameter, using the 'SpaceDelimited' style. + +.EXAMPLE + Get-PodeQueryParameter -Name 'data' -Deserialize -NoExplode + Deserializes the 'data' query parameter value without exploding arrays. + +.NOTES + This function should be used within a route's script block in a Pode server. The `-Deserialize` switch enables + advanced handling of complex query parameter data structures. +#> +function Get-PodeQueryParameter { + [CmdletBinding(DefaultParameterSetName = 'BuiltIn' )] + param( + [Parameter(Mandatory, ParameterSetName = 'Deserialize')] + [Parameter(Mandatory, ParameterSetName = 'BuiltIn')] + [string] + $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'Deserialize')] + [switch] + $Deserialize, + + [Parameter(ParameterSetName = 'Deserialize')] + [switch] + $NoExplode, + + [Parameter(ParameterSetName = 'Deserialize')] + [ValidateSet('Simple', 'Label', 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' )] + [string] + $Style = 'Form', + + [Parameter(ParameterSetName = 'Deserialize')] + [string] + $KeyName = 'id' + ) + if ($WebEvent) { + if ($Deserialize.IsPresent) { + return ConvertFrom-PodeSerializedString -SerializedString $WebEvent.Query[$Name] -Style $Style -Explode:(!$NoExplode) -KeyName $KeyName + } + return $WebEvent.Query[$Name] + } +} + +<# +.SYNOPSIS + Retrieves the body data from the current Pode web event. + +.DESCRIPTION + The `Get-PodeBodyData` function extracts and returns the body data of the current Pode web event. + This function is designed to access the main content sent in web requests, including methods such as PUT, POST, or any other HTTP methods that support a request body. + It also supports deserialization of the body data, allowing for the interpretation of serialized content. + +.PARAMETER Deserialize + Specifies that the body data should be deserialized. When this switch is used, the body data will be interpreted + based on the provided style and other deserialization options. + +.PARAMETER NoExplode + Prevents deserialization from exploding arrays in the body data. This is useful when handling parameters that + contain comma-separated values and when array expansion is not desired. Applicable only when the `-Deserialize` + switch is used. + +.PARAMETER Style + Defines the deserialization style to use when interpreting the body data. Valid options are 'Simple', 'Label', + 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. The default is 'Form'. Applicable only + when the `-Deserialize` switch is used. + +.PARAMETER KeyName + Specifies the key name to use when deserializing the body data. The default value is 'id'. This option is useful + for mapping the body data accurately during deserialization. Applicable only when the `-Deserialize` switch is used. + +.EXAMPLE + Get-PodeBodyData + Returns the body data of the current web event. + +.EXAMPLE + Get-PodeBodyData -Deserialize -Style 'Matrix' + Retrieves and deserializes the body data using the 'Matrix' style. + +.EXAMPLE + Get-PodeBodyData -Deserialize -NoExplode + Deserializes the body data without exploding arrays. + +.NOTES + This function should be used within a route's script block in a Pode server. The `-Deserialize` switch enables + advanced handling of complex body data structures. +#> +function Get-PodeBodyData { + [CmdletBinding(DefaultParameterSetName = 'BuiltIn' )] + param( + [Parameter(Mandatory = $true, ParameterSetName = 'Deserialize')] + [switch] + $Deserialize, + + [Parameter(ParameterSetName = 'Deserialize')] + [switch] + $NoExplode, + + [Parameter(ParameterSetName = 'Deserialize')] + [ValidateSet('Simple', 'Label', 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject')] + [string] + $Style = 'Form', + + [Parameter(ParameterSetName = 'Deserialize')] + [string] + $KeyName = 'id' + ) + if ($WebEvent) { + if ($Deserialize.IsPresent) { + return ConvertFrom-PodeSerializedString -SerializedString $WebEvent.Data -Style $Style -Explode:(!$NoExplode) -KeyName $KeyName + } + return $WebEvent.Data + } } \ No newline at end of file diff --git a/tests/unit/Utility.Tests.ps1 b/tests/unit/Utility.Tests.ps1 new file mode 100644 index 000000000..9b3e69934 --- /dev/null +++ b/tests/unit/Utility.Tests.ps1 @@ -0,0 +1,596 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() + +BeforeAll { + $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' + + $PodeContext = @{ 'Server' = $null; } +} + +Describe 'ConvertFrom-PodeSerializedString' { + + Describe 'Path Parameters' { + It 'Convert Simple(Explode) style serialized string to a primitive value' { + $serialized = '5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple -Explode + $result | Should -be 5 + } + + It 'Convert Simple(Explode) style serialized string to hashtable' { + $serialized = 'role=admin,firstName=Alex' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple -Explode + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + $result.GetEnumerator() | ForEach-Object { + $expected[$_.Key] | Should -Be $_.Value + } + } + + It 'Convert Simple(Explode) style serialized string to array' { + $serialized = '3,4,5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple -Explode + $result | Should -be @(3, 4, 5) + } + + It 'Convert Simple style serialized string to a primitive value' { + $serialized = '5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple + $result | Should -be 5 + } + + It 'Convert Simple style serialized string to hashtable' { + $serialized = 'role,admin,firstName,Alex' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + $result.GetEnumerator() | ForEach-Object { + $expected[$_.Key] | Should -Be $_.Value + } + } + + It 'Convert Simple style serialized string to array' { + $serialized = '3,4,5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple + $result | Should -be @(3, 4, 5) + } + + + It 'Convert Label(Explode) style serialized string to a primitive value' { + $serialized = '.5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Label -Explode + $result | Should -be 5 + } + + It 'Convert Label(Explode) style serialized string to hashtable' { + $serialized = '.role=admin.firstName=Alex' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Label -Explode + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + $result.GetEnumerator() | ForEach-Object { + $expected[$_.Key] | Should -Be $_.Value + } + } + + It 'Convert Label(Explode) style serialized string to array' { + $serialized = '.3,4,5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Label -Explode + $result | Should -be @(3, 4, 5) + } + + It 'Convert Simple style serialized string to a primitive value' { + $serialized = '.5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Label + $result | Should -be 5 + } + + It 'Convert Label style serialized string to hashtable' { + $serialized = '.role,admin,firstName,Alex' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Label + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + $result.GetEnumerator() | ForEach-Object { + $expected[$_.Key] | Should -Be $_.Value + } + } + + It 'Convert Label style serialized string to array' { + $serialized = '.3,4,5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Label + $result | Should -be @(3, 4, 5) + } + + + + It 'Convert Matrix(Explode) style serialized string to a primitive value' { + $serialized = ';id=5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix -Explode + $result | Should -be 5 + } + + It 'Convert Matrix(Explode) style serialized string to hashtable' { + $serialized = ';role=admin;firstName=Alex' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix -Explode + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + $result.GetEnumerator() | ForEach-Object { + $expected[$_.Key] | Should -Be $_.Value + } + } + + It 'Convert Matrix(Explode) style serialized string to array' { + $serialized = ';id=3;id=4;id=5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix -Explode + $result | Should -be @(3, 4, 5) + } + + It 'Convert Simple style serialized string to a primitive value' { + $serialized = ';id=5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix + $result | Should -be 5 + } + + It 'Convert Matrix style serialized string to hashtable' { + $serialized = ';id=role,admin,firstName,Alex' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + $result.GetEnumerator() | ForEach-Object { + $expected[$_.Key] | Should -Be $_.Value + } + } + + It 'Convert Matrix style serialized string to array' { + $serialized = ';id=3,4,5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix + $result | Should -be @(3, 4, 5) + } + + + It 'Convert Matrix(Explode) style serialized string to a primitive value' { + $serialized = ';id=5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix -Explode + $result | Should -be 5 + } + + It 'Convert Matrix(Explode) style serialized string to hashtable' { + $serialized = ';role=admin;firstName=Alex' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix -Explode + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + $result.GetEnumerator() | ForEach-Object { + $expected[$_.Key] | Should -Be $_.Value + } + } + + It 'Convert Matrix(Explode) style serialized string to array' { + $serialized = ';id=3;id=4;id=5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix -Explode + $result | Should -be @(3, 4, 5) + } + + It 'Convert Matrix style serialized string to a primitive value' { + $serialized = ';id=5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix + $result | Should -be 5 + } + + It 'Convert Matrix style serialized string to hashtable' { + $serialized = ';id=role,admin,firstName,Alex' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + $result.GetEnumerator() | ForEach-Object { + $expected[$_.Key] | Should -Be $_.Value + } + } + + It 'Convert Matrix style serialized string to array' { + $serialized = ';id=3,4,5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix + $result | Should -be @(3, 4, 5) + } + } + + Describe 'Query Parameters' { + It 'Convert Form(Explode) style serialized string to a primitive value' { + $serialized = '?id=5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Form -Explode + $result | Should -be 5 + } + + It 'Convert Form(Explode) style serialized string to hashtable' { + $serialized = '?role=admin&firstName=Alex' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Form -Explode + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + $result.GetEnumerator() | ForEach-Object { + $expected[$_.Key] | Should -Be $_.Value + } + } + + It 'Convert Form(Explode) style serialized string to array' { + $serialized = '?id=3&id=4&id=5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Form -Explode + $result | Should -be @(3, 4, 5) + } + + It 'Convert Form style serialized string to a primitive value' { + $serialized = '?id=5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Form + $result | Should -be 5 + } + + It 'Convert Form style serialized string to hashtable' { + $serialized = '?id=role,admin,firstName,Alex' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Form + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + $result.GetEnumerator() | ForEach-Object { + $expected[$_.Key] | Should -Be $_.Value + } + } + + It 'Convert Form style serialized string to array' { + $serialized = '?id=3,4,5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Form + $result | Should -be @(3, 4, 5) + } + + + It 'Convert SpaceDelimited(Explode) style serialized string to array' { + $serialized = '?id=3&id=4&id=5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style SpaceDelimited -Explode + $result | Should -be @(3, 4, 5) + } + + It 'Convert SpaceDelimited style serialized string to array' { + $serialized = '?id=3%204%205' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style SpaceDelimited + $result | Should -be @(3, 4, 5) + } + + + It 'Convert pipeDelimited(Explode) style serialized string to array' { + $serialized = '?id=3&id=4&id=5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style pipeDelimited -Explode + $result | Should -be @(3, 4, 5) + } + + It 'Convert pipeDelimited style serialized string to array' { + $serialized = '?id=3|4|5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style pipeDelimited + $result | Should -be @(3, 4, 5) + } + + It 'Convert DeepObject(Explode) style serialized string to hashtable' { + $serialized = '?id[role]=admin&id[firstName]=Alex' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style DeepObject + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + $result.GetEnumerator() | ForEach-Object { + $expected[$_.Key] | Should -Be $_.Value + } + } + + It 'Convert DeepObject(Explode) style nested object serialized to hashtable' { + $serialized = '?id[role][type]=admin&id[role][level]=high&id[firstName]=Alex' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style DeepObject + $expected = @{ + role = @{ + type = 'admin' + level = 'high' + } + firstName = 'Alex' + } + $result['role'].GetEnumerator() | ForEach-Object { + $expected['role'][$_.Key] | Should -Be $_.Value + } + $result['firstName'] | Should -Be $expected['firstName'] + } + + } + + + Describe 'Header Parameters' { + It 'Convert Simple(Explode) style serialized string to a primitive value' { + $serialized = 'X-MyHeader: 5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple -Explode + $result['X-MyHeader'] | Should -be 5 + } + + It 'Convert Simple(Explode) style serialized string to hashtable' { + $serialized = 'X-MyHeader: role=admin,firstName=Alex' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple -Explode + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + $result['X-MyHeader'].GetEnumerator() | ForEach-Object { + $expected[$_.Key] | Should -Be $_.Value + } + } + + It 'Convert Simple(Explode) style serialized string to array' { + $serialized = 'X-MyHeader: 3,4,5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple -Explode + $result['X-MyHeader'] | Should -be @(3, 4, 5) + } + + It 'Convert Simple style serialized string to a primitive value' { + $serialized = 'X-MyHeader: 5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple + $result['X-MyHeader'] | Should -be 5 + } + + It 'Convert Simple style serialized string to hashtable' { + $serialized = 'X-MyHeader: role,admin,firstName,Alex' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + $result['X-MyHeader'].GetEnumerator() | ForEach-Object { + $expected[$_.Key] | Should -Be $_.Value + } + } + + It 'Convert Simple style serialized string to array' { + $serialized = 'X-MyHeader: 3,4,5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple + $result['X-MyHeader'] | Should -be @(3, 4, 5) + } + } + + Describe 'Cookie Parameters' { + It 'Convert Form(Explode) style serialized string to a primitive value' { + $serialized = 'Cookie: id=5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Form -Explode + $result['Cookie'] | Should -be 5 + } + + It 'Convert Form style serialized string to a primitive value' { + $serialized = 'Cookie: id=5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Form + $result['Cookie'] | Should -be 5 + } + + It 'Convert Form style serialized string to hashtable' { + $serialized = 'Cookie: id=role,admin,firstName,Alex' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Form + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + $result['Cookie'].GetEnumerator() | ForEach-Object { + $expected[$_.Key] | Should -Be $_.Value + } + } + + It 'Convert Form style serialized string to array' { + $serialized = 'Cookie: id=3,4,5' + $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Form + $result['Cookie'] | Should -be @(3, 4, 5) + } + } +} + + + +Describe 'ConvertTo-PodeSerializedString' { + + BeforeAll { + function SortSerializedString { + param ( + [string] $SerializedString, + [string] $Delimiter + ) + + $pairs = $SerializedString -split $Delimiter + $sortedPairs = $pairs | Sort-Object + return $sortedPairs -join $Delimiter + } + } + It 'should convert hashtable to Simple style serialized string' { + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + } + $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Simple' + $sortedResult = SortSerializedString -SerializedString $result -Delimiter ',' + $expected = 'name=value,anotherName=anotherValue' + $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ',' + $sortedResult | Should -Be $sortedExpected + } + + It 'should convert hashtable to Simple style serialized string with Explode' { + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + } + $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Simple' -Explode + $sortedResult = SortSerializedString -SerializedString $result -Delimiter '&' + $expected = 'name=value&anotherName=anotherValue' + $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter '&' + $sortedResult | Should -Be $sortedExpected + } + + It 'should convert hashtable to Label style serialized string' { + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + } + $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Label' + $result -eq '.name.value.anotherName.anotherValue' -or $result -eq '.anotherName.anotherValue.name.value' | Should -BeTrue + } + + It 'should convert hashtable to Matrix style serialized string' { + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + } + $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Matrix' + $sortedResult = SortSerializedString -SerializedString $result -Delimiter ';' + $expected = ';name=value;anotherName=anotherValue' + $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ';' + $sortedResult | Should -Be $sortedExpected + } + + It 'should convert hashtable to Query style serialized string' { + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + } + $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Query' + $sortedResult = SortSerializedString -SerializedString $result -Delimiter '&' + $expected = 'name=value&anotherName=anotherValue' + $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter '&' + $sortedResult | Should -Be $sortedExpected + } + + It 'should convert hashtable to Form style serialized string' { + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + } + $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Form' + $sortedResult = SortSerializedString -SerializedString $result -Delimiter '&' + $expected = 'name=value&anotherName=anotherValue' + $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter '&' + $sortedResult | Should -Be $sortedExpected + } + + It 'should convert hashtable to SpaceDelimited style serialized string' { + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + } + $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'SpaceDelimited' + $sortedResult = SortSerializedString -SerializedString $result -Delimiter ' ' + $expected = 'name=value anotherName=anotherValue' + $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ' ' + $sortedResult | Should -Be $sortedExpected + } + + It 'should convert hashtable to PipeDelimited style serialized string' { + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + } + $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'PipeDelimited' + $sortedResult = SortSerializedString -SerializedString $result -Delimiter '|' + $expected = 'name=value|anotherName=anotherValue' + $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter '|' + $sortedResult | Should -Be $sortedExpected + } + + It 'should convert hashtable to DeepObject style serialized string' { + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + } + $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'DeepObject' + $sortedResult = SortSerializedString -SerializedString $result -Delimiter ',' + $expected = 'name[name]=value,anotherName[anotherName]=anotherValue' + $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ',' + $sortedResult | Should -Be $sortedExpected + } + + It 'should convert hashtable to DeepObject style serialized string with Explode' { + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + } + $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'DeepObject' -Explode + $sortedResult = SortSerializedString -SerializedString $result -Delimiter '&' + $expected = 'name[name]=value&anotherName[anotherName]=anotherValue' + $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter '&' + $sortedResult | Should -Be $sortedExpected + } + + It 'should throw an error for unsupported serialization type' { + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + } + { ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Unsupported' } | Should -Throw + } +} + + +Describe 'Get-PodePathParameter' { + BeforeEach { + # Mock the $WebEvent variable + $Script:WebEvent = [PSCustomObject]@{ + Parameters = @{ 'action' = 'create' } + } + } + + It 'should return the specified parameter value from the web event' { + # Call the function + $result = Get-PodePathParameter -Name 'action' + + # Assert the result + $result | Should -Be 'create' + } +} + + +Describe 'Get-PodeQueryParameter' { + BeforeEach { + # Mock the $WebEvent variable + $Script:WebEvent = [PSCustomObject]@{ + Query = @{ 'userId' = '12345' } + } + } + + It 'should return the specified query parameter value from the web event' { + # Call the function + $result = Get-PodeQueryParameter -Name 'userId' + + # Assert the result + $result | Should -Be '12345' + } +} + + +Describe 'Get-PodeBodyData' { + BeforeEach { + # Mock the $WebEvent variable + $Script:WebEvent = [PSCustomObject]@{ + Data = 'This is the body data' + } + } + + It 'should return the body data of the web event' { + # Call the function + $result = Get-PodeBodyData + + # Assert the result + $result | Should -Be 'This is the body data' + } +} \ No newline at end of file From 9bfd1d318031a4eebd4671811b3b8926446b1a2d Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 10 Sep 2024 17:08:06 -0700 Subject: [PATCH 2/8] additional documentation --- docs/Tutorials/Routes/Parameters/Body.md | 2 +- docs/Tutorials/Routes/Parameters/Cookies.md | 11 +++++++++++ docs/Tutorials/Routes/Parameters/Headers.md | 9 +++++++++ docs/Tutorials/Routes/Parameters/Paths.md | 13 +++++++++++++ docs/Tutorials/Routes/Parameters/Queries.md | 16 ++++++++++++++++ 5 files changed, 50 insertions(+), 1 deletion(-) diff --git a/docs/Tutorials/Routes/Parameters/Body.md b/docs/Tutorials/Routes/Parameters/Body.md index 3cf33a0f4..b620d3a54 100644 --- a/docs/Tutorials/Routes/Parameters/Body.md +++ b/docs/Tutorials/Routes/Parameters/Body.md @@ -65,7 +65,7 @@ Start-PodeServer { ### Deserialization with Get-PodeBodyData -The `Get-PodeBodyData` function can also deserialize body data from requests, allowing for more complex data handling scenarios. This feature is especially useful when dealing with serialized data structures that require specific interpretation styles. +Typically the request body is encoded in Json,Xml or Yaml but if it's required the `Get-PodeBodyData` function can also deserialize body data from requests, allowing for more complex data handling scenarios where the only allowed ContentTypes are `application/x-www-form-urlencoded` or `multipart/form-data`. This feature can be especially useful when dealing with serialized data structures that require specific interpretation styles. To enable deserialization, use the `-Deserialize` switch along with the following options: diff --git a/docs/Tutorials/Routes/Parameters/Cookies.md b/docs/Tutorials/Routes/Parameters/Cookies.md index 60acf7445..7808ea195 100644 --- a/docs/Tutorials/Routes/Parameters/Cookies.md +++ b/docs/Tutorials/Routes/Parameters/Cookies.md @@ -66,6 +66,17 @@ To enable deserialization, use the `-Deserialize` switch along with the followin - **`-NoExplode`**: Prevents deserialization from exploding arrays in the cookie value. This is useful when handling comma-separated values where array expansion is not desired. - **`-Deserialize`**: Indicates that the retrieved cookie value should be deserialized, interpreting the content based on the provided deserialization style and options. + + +#### Supported Deserialization Styles + +| Style | Explode | URI Template | Primitive Value (id = 5) | Array (id = [3, 4, 5]) | Object (id = {"role": "admin", "firstName": "Alex"}) | +|-------|---------|--------------|--------------------------|------------------------|------------------------------------------------------| +| form* | true* | | Cookie: id=5 | | | +| form | false | id={id} | Cookie: id=5 | Cookie: id=3,4,5 | Cookie: id=role,admin,firstName,Alex | + +\* Default serialization method + ### Example with Deserialization This example demonstrates deserialization of a cookie value: diff --git a/docs/Tutorials/Routes/Parameters/Headers.md b/docs/Tutorials/Routes/Parameters/Headers.md index 46ff05fa9..9ed847080 100644 --- a/docs/Tutorials/Routes/Parameters/Headers.md +++ b/docs/Tutorials/Routes/Parameters/Headers.md @@ -63,6 +63,15 @@ To enable deserialization, use the `-Deserialize` switch along with the followin - **`-Explode`**: Specifies whether the deserialization process should explode arrays in the header value. This is useful when handling comma-separated values within the header. - **`-Deserialize`**: Indicates that the retrieved header value should be deserialized, interpreting the content based on the deserialization style and options. +#### Supported Deserialization Styles + +| Style | Explode | URI Template | Primitive Value (X-MyHeader = 5) | Array (X-MyHeader = [3, 4, 5]) | Object (X-MyHeader = {"role": "admin", "firstName": "Alex"}) | +|---------|---------|--------------|----------------------------------|--------------------------------|--------------------------------------------------------------| +| simple* | false* | {id} | X-MyHeader: 5 | X-MyHeader: 3,4,5 | X-MyHeader: role,admin,firstName,Alex | +| simple | true | {id*} | X-MyHeader: 5 | X-MyHeader: 3,4,5 | X-MyHeader: role=admin,firstName=Alex | + +\* Default serialization method + ### Example with Deserialization This example demonstrates deserialization of a header value: diff --git a/docs/Tutorials/Routes/Parameters/Paths.md b/docs/Tutorials/Routes/Parameters/Paths.md index f26bbba87..92dd95d5d 100644 --- a/docs/Tutorials/Routes/Parameters/Paths.md +++ b/docs/Tutorials/Routes/Parameters/Paths.md @@ -66,6 +66,19 @@ To enable deserialization, use the `-Deserialize` switch along with the followin - **`-Style`**: Defines the deserialization style (`'Simple'`, `'Label'`, or `'Matrix'`) to interpret the parameter value correctly. The default style is `'Simple'`. - **`-KeyName`**: Specifies the key name to use when deserializing, allowing you to map the parameter data accurately. The default value for `KeyName` is `'id'`. +#### Supported Deserialization Styles + +| Style | Explode | URI Template | Primitive Value (id = 5) | Array (id = [3, 4, 5]) | Object (id = {"role": "admin", "firstName": "Alex"}) | +|---------|---------|---------------|--------------------------|------------------------|------------------------------------------------------| +| simple* | false* | /users/{id} | /users/5 | /users/3,4,5 | /users/role,admin,firstName,Alex | +| simple | true | /users/{id*} | /users/5 | /users/3,4,5 | /users/role=admin,firstName=Alex | +| label | false | /users/{.id} | /users/.5 | /users/.3,4,5 | /users/.role,admin,firstName,Alex | +| label | true | /users/{.id*} | /users/.5 | /users/.3.4.5 | /users/.role=admin.firstName=Alex | +| matrix | false | /users/{;id} | /users/;id=5 | /users/;id=3,4,5 | /users/;id=role,admin,firstName,Alex | +| matrix | true | /users/{;id*} | /users/;id=5 | /users/;id=3;id=4;id=5 | /users/;role=admin;firstName=Alex | + +\* Default serialization method + #### Example with Deserialization This example demonstrates deserialization of a parameter that is styled and exploded as part of the request: diff --git a/docs/Tutorials/Routes/Parameters/Queries.md b/docs/Tutorials/Routes/Parameters/Queries.md index 6b9b99245..a421535dc 100644 --- a/docs/Tutorials/Routes/Parameters/Queries.md +++ b/docs/Tutorials/Routes/Parameters/Queries.md @@ -65,6 +65,22 @@ To enable deserialization, use the `-Deserialize` switch along with the followin - **`-Style`**: Defines the deserialization style (`'Simple'`, `'Label'`, `'Matrix'`, `'Form'`, `'SpaceDelimited'`, `'PipeDelimited'`, `'DeepObject'`) to interpret the query parameter value correctly. The default style is `'Form'`. - **`-KeyName`**: Specifies the key name to use when deserializing, allowing you to map the query parameter data accurately. The default value for `KeyName` is `'id'`. +#### Supported Deserialization Styles + + +| Style | Explode | URI Template | Primitive Value (id = 5) | Array (id = [3, 4, 5]) | Object (id = {"role": "admin", "firstName": "Alex"}) | +|----------------|---------|--------------|--------------------------|------------------------|------------------------------------------------------| +| form* | true* | /users{?id*} | /users?id=5 | /users?id=3&id=4&id=5 | /users?role=admin&firstName=Alex | +| form | false | /users{?id} | /users?id=5 | /users?id=3,4,5 | /users?id=role,admin,firstName,Alex | +| spaceDelimited | true | /users{?id*} | n/a | /users?id=3&id=4&id=5 | n/a | +| spaceDelimited | false | n/a | n/a | /users?id=3%204%205 | n/a | +| pipeDelimited | true | /users{?id*} | n/a | /users?id=3&id=4&id=5 | n/a | +| pipeDelimited | false | n/a | n/a | /users?id=3|4|5 | n/a | +| deepObject | true | n/a | n/a | n/a | /users?id[role]=admin&id[firstName]=Alex | + + +\* Default serialization method + #### Example with Deserialization This example demonstrates deserialization of a query parameter with specific styles and options: From 6b6f8e209025416b96ed20d5daf9203e42bbcdcf Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 14 Sep 2024 22:21:42 -0700 Subject: [PATCH 3/8] Improved ConvertTo-PodeSerializedString + added additional tests --- docs/Tutorials/Routes/Parameters/Queries.md | 6 +- src/Public/Utilities.ps1 | 131 +++++++++--- tests/unit/Utility.Tests.ps1 | 221 +++++++++++++++----- 3 files changed, 280 insertions(+), 78 deletions(-) diff --git a/docs/Tutorials/Routes/Parameters/Queries.md b/docs/Tutorials/Routes/Parameters/Queries.md index a421535dc..4c6ee93da 100644 --- a/docs/Tutorials/Routes/Parameters/Queries.md +++ b/docs/Tutorials/Routes/Parameters/Queries.md @@ -70,13 +70,13 @@ To enable deserialization, use the `-Deserialize` switch along with the followin | Style | Explode | URI Template | Primitive Value (id = 5) | Array (id = [3, 4, 5]) | Object (id = {"role": "admin", "firstName": "Alex"}) | |----------------|---------|--------------|--------------------------|------------------------|------------------------------------------------------| -| form* | true* | /users{?id*} | /users?id=5 | /users?id=3&id=4&id=5 | /users?role=admin&firstName=Alex | +| form* | true* | /users{?id*} | /users?id=5 | /users?id=3&id=4&id=5 | /users?role=admin&firstName=Alex | | form | false | /users{?id} | /users?id=5 | /users?id=3,4,5 | /users?id=role,admin,firstName,Alex | | spaceDelimited | true | /users{?id*} | n/a | /users?id=3&id=4&id=5 | n/a | | spaceDelimited | false | n/a | n/a | /users?id=3%204%205 | n/a | | pipeDelimited | true | /users{?id*} | n/a | /users?id=3&id=4&id=5 | n/a | -| pipeDelimited | false | n/a | n/a | /users?id=3|4|5 | n/a | -| deepObject | true | n/a | n/a | n/a | /users?id[role]=admin&id[firstName]=Alex | +| pipeDelimited | false | n/a | n/a | /users?id=3\|4\|5 | n/a | +| deepObject | true | n/a | n/a | n/a | /users?id[role]=admin&id[firstName]=Alex | \* Default serialization method diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index ceefd03ef..2a6d01666 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -1406,11 +1406,11 @@ function ConvertTo-PodeSerializedString { param ( [Parameter(Mandatory, ValueFromPipeline = $true, Position = 0)] - [hashtable[]] + [psobject[]] $Hashtable, [Parameter()] - [ValidateSet('Simple', 'Label', 'Matrix', 'Query', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' )] + [ValidateSet('Simple', 'Label', 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' )] [string] $Style = 'Simple', @@ -1434,45 +1434,126 @@ function ConvertTo-PodeSerializedString { $Hashtables = $Hashtable } $serializedArray = @() - foreach ( $Hashtable in $Hashtables) { - switch ($Style) { - 'Simple' { - if ($Explode) { - $serializedArray += ($Hashtable.Keys | ForEach-Object { "$($_)=$($Hashtable.""$_"")" }) -join '&' + + if ($Hashtables[0] -is [hashtable]) { + foreach ( $Hashtable in $Hashtables) { + switch ($Style) { + 'Simple' { + if ($Explode) { + $serializedArray += ($Hashtable.Keys | ForEach-Object { "$($_)=$($Hashtable.""$_"")" }) -join ',' + } + else { + $serializedArray += ($Hashtable.Keys | ForEach-Object { "$($_),$($Hashtable.""$_"")" }) -join ',' + } + break } - else { - $serializedArray += ($Hashtable.Keys | ForEach-Object { "$($_)=$($Hashtable.""$_"")" }) -join ',' + 'Label' { + if ($Explode) { + $serializedArray += ".$(($Hashtable.Keys | ForEach-Object { "$_=$($Hashtable.""$_"")" }) -join ',')" + } + else { + $serializedArray += ".$(($Hashtable.Keys | ForEach-Object { "$_,$($Hashtable.""$_"")" }) -join ',')" + } + break } + 'Matrix' { + if ($Explode) { + $serializedArray += ($Hashtable.Keys | ForEach-Object { ";$_=$($Hashtable.""$_"")" }) -join '' + } + else { + $serializedArray += ";id=$(($Hashtable.Keys | ForEach-Object { "$_,$($Hashtable.""$_"")" }) -join ',')" + } + break + } + + 'Form' { + if ($Explode) { + $serializedArray += "?$(($Hashtable.Keys | ForEach-Object { "$($_)=$($Hashtable.""$_"")" }) -join '&')" + } + else { + $serializedArray += "?id=$(($Hashtable.Keys | ForEach-Object { "$($_),$($Hashtable.""$_"")" }) -join ',')" + } + break + } + + 'DeepObject' { + $serializedArray += "?$(($Hashtable.Keys | ForEach-Object { "id[$($_)]=$($Hashtable.""$_"")" }) -join '&')" + break + } + + # Not defined by RFC + 'SpaceDelimited' { + $serializedArray += '' + } + # Not defined by RFC + 'PipeDelimited' { + $serializedArray += '' + } + + } + } + } + else { + switch ($Style) { + 'Simple' { + # explode and not explode return the same result + $serializedArray += $Hashtables -join ',' + break } 'Label' { - $serializedArray += ($Hashtable.Keys | ForEach-Object { ".$_.$($Hashtable.""$_"")" }) -join '' + # explode and not explode return the same result + $serializedArray += ".$($Hashtables -join ',')" + + break } 'Matrix' { - $serializedArray += ($Hashtable.Keys | ForEach-Object { ";$_=$($Hashtable.""$_"")" }) -join '' - } - 'Query' { - $serializedArray += ($Hashtable.Keys | ForEach-Object { "$($_)=$($Hashtable.""$_"")" }) -join '&' - } - 'Form' { - $serializedArray += ($Hashtable.Keys | ForEach-Object { "$($_)=$($Hashtable.""$_"")" }) -join '&' + if ($Explode) { + $serializedArray += ";id=$($Hashtables -join ';id=')" + } + else { + $serializedArray += ";id=$($Hashtables -join ',')" + } + break } 'SpaceDelimited' { - $serializedArray += ($Hashtable.Keys | ForEach-Object { "$($_)=$($Hashtable.""$_"")" }) -join ' ' + if ($Explode) { + $serializedArray += "?id=$($Hashtables -join '&id=')" + } + else { + $serializedArray += "?id=$($Hashtables -join '%20')" + } + break } + 'PipeDelimited' { - $serializedArray += ($Hashtable.Keys | ForEach-Object { "$($_)=$($Hashtable.""$_"")" }) -join '|' - } - 'DeepObject' { if ($Explode) { - $serializedArray += ($Hashtable.Keys | ForEach-Object { "$($_)[$($_)]=$($Hashtable.""$_"")" }) -join '&' + $serializedArray += "?id=$($Hashtables -join '&id=')" } else { - $serializedArray += ($Hashtable.Keys | ForEach-Object { "$($_)[$($_)]=$($Hashtable.""$_"")" }) -join ',' + $serializedArray += "?id=$($Hashtables -join '|')" } + break } + + 'Form' { + if ($Explode) { # Not defined by RFC + $serializedArray += '' + } + else { + $serializedArray += "id=$($Hashtables -join ',')" + } + break + } + + # Not defined by RFC + 'DeepObject' { + $serializedArray += '' + } + + } } - return $serializedArray -join '&' + return $serializedArray } } @@ -2058,7 +2139,7 @@ function ConvertFrom-PodeSerializedString { # Return the constructed hashtable with nested keys and values return $obj } - + } } } diff --git a/tests/unit/Utility.Tests.ps1 b/tests/unit/Utility.Tests.ps1 index 9b3e69934..ab9619528 100644 --- a/tests/unit/Utility.Tests.ps1 +++ b/tests/unit/Utility.Tests.ps1 @@ -407,23 +407,80 @@ Describe 'ConvertTo-PodeSerializedString' { function SortSerializedString { param ( [string] $SerializedString, - [string] $Delimiter + [string] $Delimiter, + [switch] $GroupPairs, + [string] $SkipHead = '', + [string] $RemovePattern = '' ) - $pairs = $SerializedString -split $Delimiter - $sortedPairs = $pairs | Sort-Object - return $sortedPairs -join $Delimiter + # If a head to skip is specified, separate it from the rest of the string + if ($SkipHead -and $SerializedString.StartsWith($SkipHead)) { + # Extract the head and the rest of the string + $head = $SkipHead + $SerializedString = $SerializedString.Substring($SkipHead.Length) + } + else { + $head = '' + } + + # Split the remaining string into individual elements + $elements = $SerializedString -split $Delimiter + + # Apply pattern removal if specified + if ($RemovePattern) { + $elements = $elements.ForEach({ + $_ -replace $RemovePattern, '' + }) + } + + if ($GroupPairs) { + # Group elements into pairs (key-value) + $pairs = for ($i = 0; $i -lt $elements.Count; $i += 2) { + # Check if the next element exists to avoid a trailing delimiter + if ($i + 1 -lt $elements.Count) { + "$($elements[$i])$Delimiter$($elements[$i + 1])" + } + else { + # If the last element doesn't have a pair, add it as is + $elements[$i] + } + } + + # Sort the pairs + $sortedPairs = $pairs | Sort-Object + + # Join sorted pairs back into a single string + $sortedString = $sortedPairs -join $Delimiter + } + else { + # Sort elements individually without grouping into pairs + $sortedElements = $elements | Sort-Object + + # Join sorted elements back into a single string + $sortedString = $sortedElements -join $Delimiter + } + + # Reattach the head (if any) at the start of the sorted string + $result = "$head$sortedString" + + # Remove any trailing delimiter that may have been inadvertently added + if ($result.EndsWith($Delimiter)) { + $result = $result.Substring(0, $result.Length - 1) + } + + return $result } } It 'should convert hashtable to Simple style serialized string' { $hashtable = @{ name = 'value' anotherName = 'anotherValue' + number = 10 } $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Simple' - $sortedResult = SortSerializedString -SerializedString $result -Delimiter ',' - $expected = 'name=value,anotherName=anotherValue' - $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ',' + $sortedResult = SortSerializedString -SerializedString $result -Delimiter ',' -GroupPairs + $expected = 'name,value,number,10,anotherName,anotherValue' + $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ',' -GroupPairs $sortedResult | Should -Be $sortedExpected } @@ -431,11 +488,12 @@ Describe 'ConvertTo-PodeSerializedString' { $hashtable = @{ name = 'value' anotherName = 'anotherValue' + number = 10 } $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Simple' -Explode - $sortedResult = SortSerializedString -SerializedString $result -Delimiter '&' - $expected = 'name=value&anotherName=anotherValue' - $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter '&' + $sortedResult = SortSerializedString -SerializedString $result -Delimiter ',' + $expected = 'name=value,number=10,anotherName=anotherValue' + $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ',' $sortedResult | Should -Be $sortedExpected } @@ -443,99 +501,162 @@ Describe 'ConvertTo-PodeSerializedString' { $hashtable = @{ name = 'value' anotherName = 'anotherValue' + number = 10 } $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Label' - $result -eq '.name.value.anotherName.anotherValue' -or $result -eq '.anotherName.anotherValue.name.value' | Should -BeTrue - } - It 'should convert hashtable to Matrix style serialized string' { - $hashtable = @{ - name = 'value' - anotherName = 'anotherValue' - } - $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Matrix' - $sortedResult = SortSerializedString -SerializedString $result -Delimiter ';' - $expected = ';name=value;anotherName=anotherValue' - $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ';' + $sortedResult = SortSerializedString -SerializedString $result -Delimiter ',' -SkipHead '.' + $expected = '.anotherName,anotherValue,number,10,name,value' + $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ',' -SkipHead '.' $sortedResult | Should -Be $sortedExpected } - It 'should convert hashtable to Query style serialized string' { + It 'should convert hashtable to Label style serialized string with Explode' { $hashtable = @{ name = 'value' anotherName = 'anotherValue' + number = 10 } - $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Query' - $sortedResult = SortSerializedString -SerializedString $result -Delimiter '&' - $expected = 'name=value&anotherName=anotherValue' - $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter '&' + $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Label' -Explode + $sortedResult = SortSerializedString -SerializedString $result -Delimiter ',' -SkipHead '.' + $expected = '.anotherName=anotherValue,number=10,name=value' + $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ',' -SkipHead '.' $sortedResult | Should -Be $sortedExpected } - It 'should convert hashtable to Form style serialized string' { + It 'should convert hashtable to Matrix style serialized string' { $hashtable = @{ name = 'value' anotherName = 'anotherValue' + number = 10 } - $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Form' - $sortedResult = SortSerializedString -SerializedString $result -Delimiter '&' - $expected = 'name=value&anotherName=anotherValue' - $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter '&' + $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Matrix' + $sortedResult = SortSerializedString -SerializedString $result -Delimiter ',' -GroupPairs -SkipHead ';id=' + $expected = ';id=name,value,number,10,anotherName,anotherValue' + $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ',' -GroupPairs -SkipHead ';id=' $sortedResult | Should -Be $sortedExpected } - It 'should convert hashtable to SpaceDelimited style serialized string' { + It 'should convert hashtable to Matrix style serialized string with Explode' { $hashtable = @{ name = 'value' anotherName = 'anotherValue' + number = 10 } - $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'SpaceDelimited' - $sortedResult = SortSerializedString -SerializedString $result -Delimiter ' ' - $expected = 'name=value anotherName=anotherValue' - $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ' ' + $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Matrix' -Explode + $sortedResult = SortSerializedString -SerializedString $result -Delimiter ';' -SkipHead ';' + $expected = ';name=value;number=10;anotherName=anotherValue' + $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ';' -SkipHead ';' $sortedResult | Should -Be $sortedExpected } - It 'should convert hashtable to PipeDelimited style serialized string' { + It 'should convert hashtable to Form style serialized string' { $hashtable = @{ name = 'value' anotherName = 'anotherValue' + number = 10 } - $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'PipeDelimited' - $sortedResult = SortSerializedString -SerializedString $result -Delimiter '|' - $expected = 'name=value|anotherName=anotherValue' - $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter '|' + $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Form' + $sortedResult = SortSerializedString -SerializedString $result -Delimiter ',' -GroupPairs -SkipHead '?id=' + $expected = '?id=name,value,number,10,anotherName,anotherValue' + $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ',' -GroupPairs -SkipHead '?id=' $sortedResult | Should -Be $sortedExpected } - It 'should convert hashtable to DeepObject style serialized string' { + It 'should convert hashtable to Form style serialized string with Explode' { $hashtable = @{ name = 'value' anotherName = 'anotherValue' + number = 10 } - $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'DeepObject' - $sortedResult = SortSerializedString -SerializedString $result -Delimiter ',' - $expected = 'name[name]=value,anotherName[anotherName]=anotherValue' - $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ',' + $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Form' -Explode + $sortedResult = SortSerializedString -SerializedString $result -Delimiter '&' -SkipHead '?' + $expected = '?name=value&number=10&anotherName=anotherValue' + $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter '&' -SkipHead '?' $sortedResult | Should -Be $sortedExpected } - It 'should convert hashtable to DeepObject style serialized string with Explode' { + + + It 'should convert hashtable to DeepObject style serialized string' { $hashtable = @{ name = 'value' anotherName = 'anotherValue' + number = 10 } $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'DeepObject' -Explode - $sortedResult = SortSerializedString -SerializedString $result -Delimiter '&' - $expected = 'name[name]=value&anotherName[anotherName]=anotherValue' - $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter '&' + $sortedResult = SortSerializedString -SerializedString $result -Delimiter '&' -SkipHead '?' -RemovePattern 'id\[|\]' + $expected = '?id[name]=value&id[number]=10&id[anotherName]=anotherValue' + $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter '&' -SkipHead '?' -RemovePattern 'id\[|\]' $sortedResult | Should -Be $sortedExpected } + It 'should convert array to Simple style serialized string' { + $array = @(3, 4, 5) + $result = $array |ConvertTo-PodeSerializedString -Style 'Simple' + $result | Should -Be '3,4,5' + } + + It 'should convert array to Simple style serialized string with Explode' { + $array = @(3, 4, 5) + $result = $array |ConvertTo-PodeSerializedString -Style 'Simple' -Explode + $result | Should -Be '3,4,5' + } + + It 'should convert array to Label style serialized string' { + $array = @(3, 4, 5) + $result = $array |ConvertTo-PodeSerializedString -Style 'Label' + $result | Should -Be '.3,4,5' + } + + It 'should convert array to Label style serialized string with Explode' { + $array = @(3, 4, 5) + $result = $array |ConvertTo-PodeSerializedString -Style 'Label' -Explode + $result | Should -Be '.3,4,5' + } + + It 'should convert array to Matrix style serialized string' { + $array = @(3, 4, 5) + $result = $array |ConvertTo-PodeSerializedString -Style 'Matrix' + $result | Should -Be ';id=3,4,5' + } + + It 'should convert array to Matrix style serialized string with Explode' { + $array = @(3, 4, 5) + $result = $array |ConvertTo-PodeSerializedString -Style 'Matrix' -Explode + $result | Should -Be ';id=3;id=4;id=5' + } + + It 'should convert array to SpaceDelimited style serialized string' { + $array = @(3, 4, 5) + $result = $array |ConvertTo-PodeSerializedString -Style 'SpaceDelimited' + $result | Should -Be '?id=3%204%205' + } + + It 'should convert array to SpaceDelimited style serialized string with Explode' { + $array = @(3, 4, 5) + $result = $array |ConvertTo-PodeSerializedString -Style 'SpaceDelimited' -Explode + $result | Should -Be '?id=3&id=4&id=5' + } + + It 'should convert array to PipeDelimited style serialized string' { + $array = @(3, 4, 5) + $result = $array |ConvertTo-PodeSerializedString -Style 'PipeDelimited' + $result | Should -Be '?id=3|4|5' + } + + It 'should convert array to PipeDelimited style serialized string with Explode' { + $array = @(3, 4, 5) + $result = $array |ConvertTo-PodeSerializedString -Style 'PipeDelimited' -Explode + $result | Should -Be '?id=3&id=4&id=5' + } + + It 'should throw an error for unsupported serialization type' { $hashtable = @{ name = 'value' anotherName = 'anotherValue' + number = 10 } { ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Unsupported' } | Should -Throw } From c95484b3836f61d3c445678c3b83fdd25b005140 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 14 Sep 2024 22:30:19 -0700 Subject: [PATCH 4/8] add rfc6570 link --- docs/Tutorials/Routes/Parameters/Cookies.md | 2 ++ docs/Tutorials/Routes/Parameters/Headers.md | 2 ++ docs/Tutorials/Routes/Parameters/Paths.md | 4 +++- docs/Tutorials/Routes/Parameters/Queries.md | 4 +++- src/Public/Utilities.ps1 | 18 ++++++++++++++---- 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/docs/Tutorials/Routes/Parameters/Cookies.md b/docs/Tutorials/Routes/Parameters/Cookies.md index 7808ea195..f1222aa2e 100644 --- a/docs/Tutorials/Routes/Parameters/Cookies.md +++ b/docs/Tutorials/Routes/Parameters/Cookies.md @@ -102,4 +102,6 @@ Start-PodeServer { In this example, `Get-PodeCookie` is used to deserialize the `Session` cookie, interpreting it according to the provided deserialization options. The `-NoExplode` switch ensures that any arrays within the cookie value are not expanded during deserialization. +For further information regarding serialization, please refer to the [RFC6570](https://tools.ietf.org/html/rfc6570). + For further information on general usage and retrieving cookies, please refer to the [Headers Documentation](Cookies.md). \ No newline at end of file diff --git a/docs/Tutorials/Routes/Parameters/Headers.md b/docs/Tutorials/Routes/Parameters/Headers.md index 9ed847080..913b860bf 100644 --- a/docs/Tutorials/Routes/Parameters/Headers.md +++ b/docs/Tutorials/Routes/Parameters/Headers.md @@ -97,4 +97,6 @@ Start-PodeServer { In this example, `Get-PodeHeader` is used to deserialize the `X-SerializedHeader` header, interpreting it according to the provided deserialization options. The `-Explode` switch ensures that any arrays within the header value are properly expanded during deserialization. +For further information regarding serialization, please refer to the [RFC6570](https://tools.ietf.org/html/rfc6570). + For further information on general usage and retrieving headers, please refer to the [Headers Documentation](Headers.md). diff --git a/docs/Tutorials/Routes/Parameters/Paths.md b/docs/Tutorials/Routes/Parameters/Paths.md index 92dd95d5d..e1a3d3e5a 100644 --- a/docs/Tutorials/Routes/Parameters/Paths.md +++ b/docs/Tutorials/Routes/Parameters/Paths.md @@ -103,4 +103,6 @@ Start-PodeServer { } ``` -In this example, the `Get-PodePathParameter` function is used to deserialize the `itemId` parameter, interpreting it according to the specified style (`Label`) and handling arrays if present (`-Explode`). The default `KeyName` is `'id'`, but it can be customized as needed. This approach allows for dynamic and precise handling of incoming request data, making your Pode routes more versatile and resilient. \ No newline at end of file +In this example, the `Get-PodePathParameter` function is used to deserialize the `itemId` parameter, interpreting it according to the specified style (`Label`) and handling arrays if present (`-Explode`). The default `KeyName` is `'id'`, but it can be customized as needed. This approach allows for dynamic and precise handling of incoming request data, making your Pode routes more versatile and resilient. + +For further information regarding serialization, please refer to the [RFC6570](https://tools.ietf.org/html/rfc6570). \ No newline at end of file diff --git a/docs/Tutorials/Routes/Parameters/Queries.md b/docs/Tutorials/Routes/Parameters/Queries.md index 4c6ee93da..41b839b98 100644 --- a/docs/Tutorials/Routes/Parameters/Queries.md +++ b/docs/Tutorials/Routes/Parameters/Queries.md @@ -102,4 +102,6 @@ Start-PodeServer { } ``` -In this example, the `Get-PodeQueryParameter` function is used to deserialize the `filter` query parameter, interpreting it according to the specified style (`SpaceDelimited`) and preventing array explosion (`-NoExplode`). This approach allows for dynamic and precise handling of complex query data, enhancing the flexibility of your Pode routes. \ No newline at end of file +In this example, the `Get-PodeQueryParameter` function is used to deserialize the `filter` query parameter, interpreting it according to the specified style (`SpaceDelimited`) and preventing array explosion (`-NoExplode`). This approach allows for dynamic and precise handling of complex query data, enhancing the flexibility of your Pode routes. + +For further information regarding serialization, please refer to the [RFC6570](https://tools.ietf.org/html/rfc6570). \ No newline at end of file diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index 2a6d01666..b79bb7e72 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -1401,6 +1401,11 @@ function ConvertFrom-PodeXml { } $serializedExplode = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'DeepObject' -Explode Write-Output $serializedExplode + +.NOTES + Additional info regarding serialization + https://swagger.io/docs/specification/serialization/ + https://tools.ietf.org/html/rfc6570 #> function ConvertTo-PodeSerializedString { @@ -1481,11 +1486,11 @@ function ConvertTo-PodeSerializedString { break } - # Not defined by RFC + # Not defined by RFC 6570. 'SpaceDelimited' { $serializedArray += '' } - # Not defined by RFC + # Not defined by RFC 6570. 'PipeDelimited' { $serializedArray += '' } @@ -1536,7 +1541,7 @@ function ConvertTo-PodeSerializedString { } 'Form' { - if ($Explode) { # Not defined by RFC + if ($Explode) { # Not defined by RFC 6570. $serializedArray += '' } else { @@ -1545,7 +1550,7 @@ function ConvertTo-PodeSerializedString { break } - # Not defined by RFC + # Not defined by RFC 6570. 'DeepObject' { $serializedArray += '' } @@ -1670,6 +1675,11 @@ function ConvertTo-PodeSerializedString { $serialized = "myId[role]=admin&myId[firstName]=Alex" $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'DeepObject' -Explode -KeyName 'myId' Write-Output $hashtable + +.NOTES + Additional info regarding serialization + https://swagger.io/docs/specification/serialization/ + https://tools.ietf.org/html/rfc6570 #> function ConvertFrom-PodeSerializedString { param ( From bd2da283b4067d82ebc2fe733ce4374116f062ad Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sun, 15 Sep 2024 09:15:11 -0700 Subject: [PATCH 5/8] added URL encoding and decoding --- src/Public/Utilities.ps1 | 725 ++++++++++++++++++++++++----------- tests/unit/Utility.Tests.ps1 | 495 +++++++++++++++++------- 2 files changed, 877 insertions(+), 343 deletions(-) diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index b79bb7e72..546a41fff 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -1365,326 +1365,615 @@ function ConvertFrom-PodeXml { return $oHash } - <# .SYNOPSIS - Converts a hashtable to a serialized string using a specified serialization style. + Converts an object (hashtable or array) to a serialized string using a specified serialization style. .DESCRIPTION - The ConvertTo-PodeSerializedString function takes a hashtable and converts it to a serialized string - according to the specified serialization style. It supports various serialization styles such as - 'Simple', 'Label', 'Matrix', 'Query', 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. - An optional 'Explode' switch can be used to modify the serialization format for certain styles. + The `ConvertTo-PodeSerializedString` function takes a hashtable or array and converts it into a serialized string + according to the specified serialization style. It supports various styles such as 'Simple', 'Label', 'Matrix', + 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. + + By default, parameter names and values are URL-encoded to ensure safe inclusion in URLs. You can disable URL encoding + by using the `-NoUrlEncode` switch. -.PARAMETER Hashtable - The hashtable to be serialized. + An optional `-Explode` switch can be used to modify the serialization format for certain styles, altering how arrays + and objects are represented in the serialized string. + +.PARAMETER InputObject + The object to be serialized. This can be a hashtable (or ordered dictionary) or an array. Supports pipeline input. .PARAMETER Style - The style of serialization to be used. Valid values are 'Simple', 'Label', 'Matrix', 'Query', - 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. + The serialization style to use. Valid values are 'Simple', 'Label', 'Matrix', 'Form', 'SpaceDelimited', + 'PipeDelimited', and 'DeepObject'. Defaults to 'Simple'. .PARAMETER Explode - An optional switch to modify the serialization format for certain styles. + An optional switch to modify the serialization format for certain styles. When used, arrays and objects are + serialized in an expanded form. + +.PARAMETER NoUrlEncode + An optional switch to disable URL encoding of the serialized output. By default, parameter names and values are + URL-encoded individually. Use this switch if you require the output without URL encoding. + +.PARAMETER ParameterName + Specifies the name of the parameter to use in the serialized output. Defaults to 'id' if not specified. .EXAMPLE - $hashtable = @{ + $item = @{ name = 'value' anotherName = 'anotherValue' } - $serialized = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Query' + $serialized = ConvertTo-PodeSerializedString -InputObject $item -Style 'Form' Write-Output $serialized + # Output: + # ?id=name%2Cvalue%2CanotherName%2CanotherValue + .EXAMPLE - $hashtable = @{ + $item = @{ name = 'value' anotherName = 'anotherValue' } - $serializedExplode = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'DeepObject' -Explode + $serializedExplode = ConvertTo-PodeSerializedString -InputObject $item -Style 'DeepObject' -Explode Write-Output $serializedExplode + # Output: + # ?id[name]=value&id[anotherName]=anotherValue + +.EXAMPLE + $array = @('3', '4', '5') + $serialized = ConvertTo-PodeSerializedString -InputObject $array -Style 'SpaceDelimited' -Explode + Write-Output $serialized + + # Output: + # ?id=3&id=4&id=5 + +.EXAMPLE + $array = @('3', '4', '5') + $serialized = ConvertTo-PodeSerializedString -InputObject $array -Style 'SpaceDelimited' -NoUrlEncode + Write-Output $serialized + + # Output: + # ?id=3 4 5 + +.EXAMPLE + $item = @{ + 'user name' = 'Alice & Bob' + 'role' = 'Admin/User' + } + $serialized = ConvertTo-PodeSerializedString -InputObject $item -Style 'Form' -ParameterName 'account' -NoUrlEncode + Write-Output $serialized + + # Output: + # ?account=user name,Alice & Bob,role,Admin/User + .NOTES - Additional info regarding serialization - https://swagger.io/docs/specification/serialization/ - https://tools.ietf.org/html/rfc6570 + - 'SpaceDelimited' and 'PipeDelimited' styles for hashtables are not implemented as they are not defined by RFC 6570. + - The 'Form' style with 'Explode' for arrays is not implemented for the same reason. + - The 'Explode' option for 'SpaceDelimited' and 'PipeDelimited' styles for arrays is implemented as per the OpenAPI Specification. + + Additional information regarding serialization: + - OpenAPI Specification Serialization: https://swagger.io/docs/specification/serialization/ + - RFC 6570 - URI Template: https://tools.ietf.org/html/rfc6570 #> function ConvertTo-PodeSerializedString { - param ( [Parameter(Mandatory, ValueFromPipeline = $true, Position = 0)] [psobject[]] - $Hashtable, + $InputObject, [Parameter()] - [ValidateSet('Simple', 'Label', 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' )] + [ValidateSet('Simple', 'Label', 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject')] [string] $Style = 'Simple', [Parameter()] [switch] - $Explode + $Explode, + + [Parameter()] + [switch] + $NoUrlEncode, + + [Parameter()] + [string] + $ParameterName = 'id' # Default parameter name ) + begin { + # Initialize an array to collect pipeline input $pipelineValue = @() } process { + # Collect each input object from the pipeline $pipelineValue += $_ } end { + # Determine if multiple objects were provided via pipeline if ($pipelineValue.Count -gt 1) { - $Hashtables = $pipelineValue + $inputObjects = $pipelineValue } else { - $Hashtables = $Hashtable + $inputObjects = $InputObject } + + # Initialize an array to store the serialized strings $serializedArray = @() - if ($Hashtables[0] -is [hashtable]) { - foreach ( $Hashtable in $Hashtables) { + # return '' if the inputObjects is null + if($null -eq $inputObjects){ + return '' + } + + # Check if there are input objects to process + if ( $inputObjects.Count -gt 0) { + + # Check if the first input object is a hashtable or ordered dictionary + if ($inputObjects[0] -is [hashtable] -or $inputObjects[0] -is [System.Collections.Specialized.OrderedDictionary]) { + + # Process each hashtable item + foreach ($item in $inputObjects) { + switch ($Style) { + + 'Simple' { + # Handle 'Simple' style for hashtables + if ($Explode) { + # Serialize each key-value pair with '=' and join with ',' + $serializedArray += ( ($item.Keys | ForEach-Object { + $key = $_ + $value = $item[$_] + # URL-encode unless $NoUrlEncode is specified + if (-not $NoUrlEncode) { + $key = [uri]::EscapeDataString($key) + $value = [uri]::EscapeDataString($value) + } + "$key=$value" + }) -join ',' ) + } + else { + # Serialize each key-value pair with ',' and join with ',' + $serializedArray += ( ($item.Keys | ForEach-Object { + $key = $_ + $value = $item[$_] + if (-not $NoUrlEncode) { + $key = [uri]::EscapeDataString($key) + $value = [uri]::EscapeDataString($value) + } + "$key,$value" + }) -join ',' ) + } + break + } + + 'Label' { + # Handle 'Label' style for hashtables + if ($Explode) { + # Prepend '.' and serialize each key-value pair with '=' + $serializedArray += '.' + ( ($item.Keys | ForEach-Object { + $key = $_ + $value = $item[$_] + if (-not $NoUrlEncode) { + $key = [uri]::EscapeDataString($key) + $value = [uri]::EscapeDataString($value) + } + "$key=$value" + }) -join ',' ) + } + else { + # Prepend '.' and serialize each key-value pair with ',' + $serializedArray += '.' + ( ($item.Keys | ForEach-Object { + $key = $_ + $value = $item[$_] + if (-not $NoUrlEncode) { + $key = [uri]::EscapeDataString($key) + $value = [uri]::EscapeDataString($value) + } + "$key,$value" + }) -join ',' ) + } + break + } + + 'Matrix' { + # Handle 'Matrix' style for hashtables + if ($Explode) { + # Serialize each key-value pair with ';' prefix + $serializedArray += ( ($item.Keys | ForEach-Object { + $key = $_ + $value = $item[$_] + if (-not $NoUrlEncode) { + $key = [uri]::EscapeDataString($key) + $value = [uri]::EscapeDataString($value) + } + ";$key=$value" + }) -join '' ) + } + else { + # Serialize key-value pairs into a single parameter + $valueString = ( ($item.Keys | ForEach-Object { + $key = $_ + $value = $item[$_] + if (-not $NoUrlEncode) { + $key = [uri]::EscapeDataString($key) + $value = [uri]::EscapeDataString($value) + } + "$key,$value" + }) -join ',' ) + # Encode parameter name if necessary + if (-not $NoUrlEncode) { + $parameterName = [uri]::EscapeDataString($ParameterName) + } + else { + $parameterName = $ParameterName + } + $serializedArray += ";$parameterName=$valueString" + } + break + } + + 'Form' { + # Handle 'Form' style for hashtables + if ($Explode) { + # Serialize each key-value pair as query parameters + $serializedArray += '?' + ( ($item.Keys | ForEach-Object { + $key = $_ + $value = $item[$_] + if (-not $NoUrlEncode) { + $key = [uri]::EscapeDataString($key) + $value = [uri]::EscapeDataString($value) + } + "$key=$value" + }) -join '&' ) + } + else { + # Serialize key-value pairs into a single query parameter + $valueString = ( ($item.Keys | ForEach-Object { + $key = $_ + $value = $item[$_] + if (-not $NoUrlEncode) { + $key = [uri]::EscapeDataString($key) + $value = [uri]::EscapeDataString($value) + } + "$key,$value" + }) -join ',' ) + if (-not $NoUrlEncode) { + $parameterName = [uri]::EscapeDataString($ParameterName) + } + else { + $parameterName = $ParameterName + } + $serializedArray += "?$parameterName=$valueString" + } + break + } + + 'DeepObject' { + # Handle 'DeepObject' style for hashtables + # Encode parameter name once outside the loop + if (-not $NoUrlEncode) { + $parameterNameEncoded = [uri]::EscapeDataString($ParameterName) + } + else { + $parameterNameEncoded = $ParameterName + } + # Serialize each key-value pair using bracket notation + $serializedArray += '?' + ( ($item.Keys | ForEach-Object { + $key = $_ + $value = $item[$_] + if (-not $NoUrlEncode) { + $key = [uri]::EscapeDataString($key) + $value = [uri]::EscapeDataString($value) + } + "$parameterNameEncoded`[$key`]=$value" + }) -join '&' ) + break + } + + # Styles not defined for hashtables + 'SpaceDelimited' { + $serializedArray += '' + Write-Verbose "Serialization for objects using '$Style' style is not defined by RFC 6570." + } + + 'PipeDelimited' { + $serializedArray += '' + Write-Verbose "Serialization for objects using '$Style' style is not defined by RFC 6570." + } + } + } + } + else { + # Process input as an array switch ($Style) { + 'Simple' { + # Handle 'Simple' style for arrays + # Both 'Explode' and non-'Explode' result in the same output + $serializedArray += ( ($inputObjects | ForEach-Object { + $value = $_ + if (-not $NoUrlEncode) { + $value = [uri]::EscapeDataString($value) + } + $value + }) -join ',' ) + break + } + + 'Label' { + # Handle 'Label' style for arrays + $serializedArray += '.' + ( ($inputObjects | ForEach-Object { + $value = $_ + if (-not $NoUrlEncode) { + $value = [uri]::EscapeDataString($value) + } + $value + }) -join ',' ) + break + } + + 'Matrix' { + # Handle 'Matrix' style for arrays + if (-not $NoUrlEncode) { + $parameterName = [uri]::EscapeDataString($ParameterName) + } + else { + $parameterName = $ParameterName + } if ($Explode) { - $serializedArray += ($Hashtable.Keys | ForEach-Object { "$($_)=$($Hashtable.""$_"")" }) -join ',' + # Serialize each value with parameter name + $serializedArray += ';' + ( ($inputObjects | ForEach-Object { + $value = $_ + if (-not $NoUrlEncode) { + $value = [uri]::EscapeDataString($value) + } + "$parameterName=$value" + }) -join ';' ) } else { - $serializedArray += ($Hashtable.Keys | ForEach-Object { "$($_),$($Hashtable.""$_"")" }) -join ',' + # Serialize values into a single parameter + $valueString = ( ($inputObjects | ForEach-Object { + $value = $_ + if (-not $NoUrlEncode) { + $value = [uri]::EscapeDataString($value) + } + $value + }) -join ',' ) + $serializedArray += ";$parameterName=$valueString" } break } - 'Label' { + + 'SpaceDelimited' { + # Handle 'SpaceDelimited' style for arrays + if (-not $NoUrlEncode) { + $parameterName = [uri]::EscapeDataString($ParameterName) + } + else { + $parameterName = $ParameterName + } if ($Explode) { - $serializedArray += ".$(($Hashtable.Keys | ForEach-Object { "$_=$($Hashtable.""$_"")" }) -join ',')" + # Serialize each value as a separate parameter + $valueStrings = $inputObjects | ForEach-Object { + $value = $_ + if (-not $NoUrlEncode) { + $value = [uri]::EscapeDataString($value) + } + "$parameterName=$value" + } + $serializedArray += '?' + ($valueStrings -join '&') } else { - $serializedArray += ".$(($Hashtable.Keys | ForEach-Object { "$_,$($Hashtable.""$_"")" }) -join ',')" + # Join values with a space + $valueString = ($inputObjects -join ' ') + if (-not $NoUrlEncode) { + $valueString = [uri]::EscapeDataString($valueString) + } + $serializedArray += "?$parameterName=$valueString" } break } - 'Matrix' { + + 'PipeDelimited' { + # Handle 'PipeDelimited' style for arrays + if (-not $NoUrlEncode) { + $parameterName = [uri]::EscapeDataString($ParameterName) + } + else { + $parameterName = $ParameterName + } if ($Explode) { - $serializedArray += ($Hashtable.Keys | ForEach-Object { ";$_=$($Hashtable.""$_"")" }) -join '' + # Serialize each value as a separate parameter + $valueStrings = $inputObjects | ForEach-Object { + $value = $_ + if (-not $NoUrlEncode) { + $value = [uri]::EscapeDataString($value) + } + "$parameterName=$value" + } + $serializedArray += '?' + ($valueStrings -join '&') } else { - $serializedArray += ";id=$(($Hashtable.Keys | ForEach-Object { "$_,$($Hashtable.""$_"")" }) -join ',')" + # Join values with a pipe '|' + $valueString = ($inputObjects -join '|') + if (-not $NoUrlEncode) { + $valueString = [uri]::EscapeDataString($valueString) + } + $serializedArray += "?$parameterName=$valueString" } break } 'Form' { + # Handle 'Form' style for arrays + if (-not $NoUrlEncode) { + $parameterName = [uri]::EscapeDataString($ParameterName) + } + else { + $parameterName = $ParameterName + } if ($Explode) { - $serializedArray += "?$(($Hashtable.Keys | ForEach-Object { "$($_)=$($Hashtable.""$_"")" }) -join '&')" + # 'Explode' is not defined for arrays in 'Form' style + $serializedArray += '' + Write-Verbose "Serialization for array using '$Style' style with 'Explode' is not defined by RFC 6570." } else { - $serializedArray += "?id=$(($Hashtable.Keys | ForEach-Object { "$($_),$($Hashtable.""$_"")" }) -join ',')" + # Serialize values into a single parameter + $valueString = ( ($inputObjects | ForEach-Object { + $value = $_ + if (-not $NoUrlEncode) { + $value = [uri]::EscapeDataString($value) + } + $value + }) -join ',' ) + $serializedArray += "$parameterName=$valueString" } break } + # 'DeepObject' is not defined for arrays 'DeepObject' { - $serializedArray += "?$(($Hashtable.Keys | ForEach-Object { "id[$($_)]=$($Hashtable.""$_"")" }) -join '&')" - break - } - - # Not defined by RFC 6570. - 'SpaceDelimited' { $serializedArray += '' + Write-Verbose "Serialization for arrays using '$Style' style is not defined by RFC 6570." } - # Not defined by RFC 6570. - 'PipeDelimited' { - $serializedArray += '' - } - } } } - else { - switch ($Style) { - 'Simple' { - # explode and not explode return the same result - $serializedArray += $Hashtables -join ',' - break - } - 'Label' { - # explode and not explode return the same result - $serializedArray += ".$($Hashtables -join ',')" - break - } - 'Matrix' { - if ($Explode) { - $serializedArray += ";id=$($Hashtables -join ';id=')" - } - else { - $serializedArray += ";id=$($Hashtables -join ',')" - } - break - } - 'SpaceDelimited' { - if ($Explode) { - $serializedArray += "?id=$($Hashtables -join '&id=')" - } - else { - $serializedArray += "?id=$($Hashtables -join '%20')" - } - break - } - - 'PipeDelimited' { - if ($Explode) { - $serializedArray += "?id=$($Hashtables -join '&id=')" - } - else { - $serializedArray += "?id=$($Hashtables -join '|')" - } - break - } - - 'Form' { - if ($Explode) { # Not defined by RFC 6570. - $serializedArray += '' - } - else { - $serializedArray += "id=$($Hashtables -join ',')" - } - break - } - - # Not defined by RFC 6570. - 'DeepObject' { - $serializedArray += '' - } - - - } - } + # Return the serialized string(s) return $serializedArray } } + <# .SYNOPSIS - Converts a serialized string back into a hashtable, automatically detecting the serialization style. + Converts a serialized string back into its original data structure based on the specified serialization style. .DESCRIPTION - The ConvertFrom-PodeSerializedString function takes a serialized string and converts it back into a hashtable. - The function automatically detects the serialization style based on common delimiters and formats, such as - 'Simple', 'Label', 'Matrix', 'Query', 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. - If the serialized string starts with a variable name followed by a colon (e.g., X-MyHeader:), the function - will encapsulate the result within a hashtable under that variable name. + The `ConvertFrom-PodeSerializedString` function takes a serialized string and converts it back into its original data structure (e.g., hashtable, array). + The function requires the serialization style to be specified via the `-Style` parameter. + Supported styles are 'Simple', 'Label', 'Matrix', 'Query', 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. + The function also accepts an optional `-Explode` switch to indicate whether the string uses exploded serialization. + The `-ParameterName` parameter can be used to specify the key name when processing certain styles, such as 'Matrix' and 'DeepObject'. -.PARAMETER SerializedString - The serialized string to be converted back into a hashtable. +.PARAMETER SerializedInput + The serialized string to be converted back into its original data structure. .PARAMETER Style - The serialization style to use for deserialization. Options are 'Simple', 'Label', 'Matrix', 'Query', 'Form', - 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. + The serialization style to use for deserialization. Options are 'Simple', 'Label', 'Matrix', 'Query', 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. The default is 'Form'. .PARAMETER Explode - Indicates whether the string uses exploded serialization (true) or not (false). This affects how arrays and objects are handled. + Indicates whether the string uses exploded serialization (`-Explode`) or not (omit `-Explode`). This affects how arrays and objects are handled. -.PARAMETER KeyName - Specifies the key name to match when processing certain styles, such as 'Matrix' and 'DeepObject'. +.PARAMETER ParameterName + Specifies the key name to match when processing certain styles, such as 'Matrix' and 'DeepObject'. The default is 'id'. + +.PARAMETER UrlDecode + If specified, the function will decode the input string using URL decoding before processing it. This is useful + for handling serialized inputs that include URL-encoded characters, such as `%20` for spaces. .EXAMPLE # Simple style, explode = true $serialized = "name=value,anotherName=anotherValue" - $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'Simple' -Explode - Write-Output $hashtable + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Simple' -Explode + Write-Output $result .EXAMPLE # Simple style, explode = false $serialized = "name,value,anotherName,anotherValue" - $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'Simple' - Write-Output $hashtable + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Simple' + Write-Output $result .EXAMPLE # Label style, explode = true - $serialized = ".name.value.anotherName.anotherValue" - $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'Label' -Explode - Write-Output $hashtable + $serialized = ".name=value.anotherName=anotherValue" + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Label' -Explode + Write-Output $result .EXAMPLE # Label style, explode = false $serialized = ".name,value,anotherName,anotherValue" - $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'Label' - Write-Output $hashtable + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Label' + Write-Output $result .EXAMPLE # Matrix style, explode = true $serialized = ";name=value;anotherName=anotherValue" - $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'Matrix' -Explode - Write-Output $hashtable + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Matrix' -Explode + Write-Output $result .EXAMPLE # Matrix style, explode = false - $serialized = ";id=3;id=4;id=5" - $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'Matrix' - Write-Output $hashtable + $serialized = ";id=3,4,5" + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Matrix' -ParameterName 'id' + Write-Output $result .EXAMPLE # Query style, explode = true - $serialized = "name=value&anotherName=anotherValue" - $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'Query' -Explode - Write-Output $hashtable + $serialized = "?name=value&anotherName=anotherValue" + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Query' -Explode + Write-Output $result .EXAMPLE # Query style, explode = false - $serialized = "name=value,anotherName=anotherValue" - $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'Query' - Write-Output $hashtable + $serialized = "?name,value,anotherName,anotherValue" + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Query' + Write-Output $result .EXAMPLE # Form style, explode = true - $serialized = "name=value&anotherName=anotherValue" - $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'Form' -Explode - Write-Output $hashtable + $serialized = "?name=value&anotherName=anotherValue" + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Form' -Explode + Write-Output $result .EXAMPLE # Form style, explode = false - $serialized = "name=value,anotherName=anotherValue" - $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'Form' - Write-Output $hashtable + $serialized = "?name,value,anotherName,anotherValue" + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Form' + Write-Output $result .EXAMPLE # SpaceDelimited style, explode = true - $serialized = "id=3&id=4&id=5" - $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'SpaceDelimited' -Explode - Write-Output $hashtable + $serialized = "?id=3&id=4&id=5" + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'SpaceDelimited' -Explode -ParameterName 'id' + Write-Output $result .EXAMPLE # SpaceDelimited style, explode = false - $serialized = "id=3%204%205" - $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'SpaceDelimited' - Write-Output $hashtable + $serialized = "?id=3%204%205" + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'SpaceDelimited' -ParameterName 'id' + Write-Output $result .EXAMPLE # PipeDelimited style, explode = true - $serialized = "id=3&id=4&id=5" - $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'PipeDelimited' -Explode - Write-Output $hashtable + $serialized = "?id=3&id=4&id=5" + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'PipeDelimited' -Explode -ParameterName 'id' + Write-Output $result .EXAMPLE # PipeDelimited style, explode = false - $serialized = "id=3|4|5" - $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'PipeDelimited' - Write-Output $hashtable + $serialized = "?id=3|4|5" + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'PipeDelimited' -ParameterName 'id' + Write-Output $result .EXAMPLE - # DeepObject style, explode = true + # DeepObject style $serialized = "myId[role]=admin&myId[firstName]=Alex" - $hashtable = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style 'DeepObject' -Explode -KeyName 'myId' - Write-Output $hashtable + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'DeepObject' -ParameterName 'myId' + Write-Output $result .NOTES - Additional info regarding serialization - https://swagger.io/docs/specification/serialization/ - https://tools.ietf.org/html/rfc6570 + For more information on serialization styles, refer to: + - https://swagger.io/docs/specification/serialization/ + - https://tools.ietf.org/html/rfc6570 #> + function ConvertFrom-PodeSerializedString { param ( [Parameter(Mandatory, ValueFromPipeline = $true, Position = 0)] - [string] $SerializedString, + [string] $SerializedInput, [Parameter()] [ValidateSet('Simple', 'Label', 'Matrix', 'Query', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' )] @@ -1697,22 +1986,28 @@ function ConvertFrom-PodeSerializedString { [Parameter()] [string] - $KeyName = 'id' # Default key name if not specified + $ParameterName = 'id', # Default key name if not specified + + [Parameter()] + [switch] + $UrlDecode ) process { - $SerializedString = $SerializedString.Replace('%20', ' ') + if($UrlDecode){ + $SerializedInput = [System.Web.HttpUtility]::UrlDecode($SerializedInput) + } # Main deserialization logic based on style switch ($Style) { 'Simple' { # Check for header pattern and extract it if present - if ($SerializedString -match '^([a-zA-Z0-9_-]+):') { + if ($SerializedInput -match '^([a-zA-Z0-9_-]+):') { # Extract the variable name and strip it from the serialized string $headerName = $matches[1] - $SerializedString = ($SerializedString -replace "^$($headerName):", '').Trim() + $SerializedInput = ($SerializedInput -replace "^$($headerName):", '').Trim() } - $segments = $SerializedString -split ',' + $segments = $SerializedInput -split ',' # If there's only one segment, return it directly if ($segments.Count -eq 1) { @@ -1723,7 +2018,7 @@ function ConvertFrom-PodeSerializedString { # Handling explode=true case # Check if the number of '=' is equal to the count of segments - if ((($SerializedString -split '=').Count - 1) -eq $segments.Count) { + if ((($SerializedInput -split '=').Count - 1) -eq $segments.Count) { $obj = @{} foreach ($pair in $segments) { if ($pair.Contains('=')) { @@ -1770,16 +2065,16 @@ function ConvertFrom-PodeSerializedString { return @{$headerName = $result } } else { - $result + return $result } } 'Label' { # Remove the leading dot (.) prefix from the serialized string - $SerializedString = $SerializedString.TrimStart('.') + $SerializedInput = $SerializedInput.TrimStart('.') # Split the string by dot - $segments = $SerializedString -split '\.' + $segments = $SerializedInput -split '\.' # Handle the explode=true case if ($Explode) { @@ -1800,7 +2095,7 @@ function ConvertFrom-PodeSerializedString { else { # Handling explode=false: all segments form a combined structure # Split the string by commas within each segment - $combinedSegments = ($SerializedString -split ',') + $combinedSegments = ($SerializedInput -split ',') # Check if it's likely an object by checking if the count is even if ($combinedSegments.Count % 2 -eq 0) { @@ -1830,10 +2125,10 @@ function ConvertFrom-PodeSerializedString { # Handle the explode=true case if ($Explode) { # Remove the leading semicolon (;) prefix from the serialized string - $SerializedString = $SerializedString.TrimStart(';') + $SerializedInput = $SerializedInput.TrimStart(';') # Split by semicolon to get segments - $segments = $SerializedString -split ';' + $segments = $SerializedInput -split ';' # If each segment doesn't contain '=', treat it as an array if ($segments -notmatch '=') { @@ -1851,7 +2146,7 @@ function ConvertFrom-PodeSerializedString { $key, $value = $segment -split '=', 2 # If the key matches the specified key name - if ($key -eq $KeyName) { + if ($key -eq $ParameterName) { $values += $value } else { @@ -1871,25 +2166,25 @@ function ConvertFrom-PodeSerializedString { # Merge values back into the object if any key matches the KeyName if ($values.Count -gt 0) { - $obj[$KeyName] = if ($values.Count -eq 1) { $values[0] } else { $values } + $obj[$ParameterName] = if ($values.Count -eq 1) { $values[0] } else { $values } } # Return the hashtable if it contains any key-value pairs if ($obj.Count -gt 0) { - return $obj + return $obj } else { - return $values + return $values } } else { # Handling explode=false: # Remove the leading semicolon (;) prefix from the serialized string - $SerializedString = $SerializedString.TrimStart(";$KeyName=") + $SerializedInput = $SerializedInput.TrimStart(";$ParameterName=") # Split by semicolon to get segments - $segments = $SerializedString -split ',' + $segments = $SerializedInput -split ',' # If there's only one segment, return it directly if ($segments.Count -eq 1) { @@ -1922,26 +2217,26 @@ function ConvertFrom-PodeSerializedString { 'Form' { # Check for header pattern and extract it if present - if ($SerializedString -match '^([a-zA-Z0-9_-]+):') { + if ($SerializedInput -match '^([a-zA-Z0-9_-]+):') { # Extract the variable name and strip it from the serialized string $headerName = $matches[1] - $SerializedString = ($SerializedString -replace "^$($headerName):", '').Trim().TrimStart("$KeyName=") + $SerializedInput = ($SerializedInput -replace "^$($headerName):", '').Trim().TrimStart("$ParameterName=") } else { if ($Explode) { # Remove the leading semicolon (;) prefix from the serialized string - $SerializedString = $SerializedString.TrimStart('?') + $SerializedInput = $SerializedInput.TrimStart('?') } else { # Remove the leading semicolon (;) prefix from the serialized string - $SerializedString = $SerializedString.TrimStart("?$KeyName=") + $SerializedInput = $SerializedInput.TrimStart("?$ParameterName=") } } # Handle the explode=true case if ($Explode) { # Split by semicolon to get segments - $segments = $SerializedString -split '&' + $segments = $SerializedInput -split '&' # If each segment doesn't contain '=', treat it as an array if ($segments -notmatch '=') { @@ -1958,7 +2253,7 @@ function ConvertFrom-PodeSerializedString { $key, $value = $segment -split '=', 2 # If the key matches the specified key name - if ($key -eq $KeyName) { + if ($key -eq $ParameterName) { $values += $value } else { @@ -1981,15 +2276,15 @@ function ConvertFrom-PodeSerializedString { # Merge values back into the object if any key matches the KeyName if ($values.Count -gt 0) { - $obj[$KeyName] = if ($values.Count -eq 1) { $values[0] } else { $values } + $obj[$ParameterName] = if ($values.Count -eq 1) { $values[0] } else { $values } } # Return the hashtable if it contains any key-value pairs if ($obj.Count -gt 0) { - return $obj + return $obj } else { - return $values + return $values } } } @@ -1998,7 +2293,7 @@ function ConvertFrom-PodeSerializedString { # Handling explode=false # Split by semicolon to get segments - $segments = $SerializedString -split ',' + $segments = $SerializedInput -split ',' # If there's only one segment, return it directly if ($segments.Count -eq 1) { @@ -2036,17 +2331,17 @@ function ConvertFrom-PodeSerializedString { return @{$headerName = $result } } else { - $result + return $result } } 'SpaceDelimited' { if ($Explode) { # Remove the leading semicolon (;) prefix from the serialized string - $SerializedString = $SerializedString.TrimStart('?') + $SerializedInput = $SerializedInput.TrimStart('?') # For explode=true, split by '&' to treat each value as a separate occurrence - $segments = $SerializedString -split '&' + $segments = $SerializedInput -split '&' # Initialize an array to store values that match the specified KeyName $values = @() @@ -2054,7 +2349,7 @@ function ConvertFrom-PodeSerializedString { if ($segment.Contains('=')) { $key, $value = $segment -split '=', 2 # Only add values where the key matches the specified KeyName - if ($key -eq $KeyName) { + if ($key -eq $ParameterName) { $values += $value } } @@ -2063,18 +2358,18 @@ function ConvertFrom-PodeSerializedString { return $values } else { - # Remove the leading semicolon (;) prefix from the serialized string - $SerializedString = $SerializedString.TrimStart('?id=') + # Remove the leading semicolon '?id=' prefix from the serialized string + $SerializedInput = $SerializedInput.TrimStart('?id=') # For explode=false, split by space (%20) to handle the combined string format - return $SerializedString -split ' ' + return $SerializedInput -split ' ' } } 'PipeDelimited' { if ($Explode) { - $SerializedString = $SerializedString.TrimStart('?') + $SerializedInput = $SerializedInput.TrimStart('?') # For explode=true, split by '&' to treat each value as a separate occurrence - $segments = $SerializedString -split '&' + $segments = $SerializedInput -split '&' # Initialize an array to store values that match the specified KeyName $values = @() @@ -2082,7 +2377,7 @@ function ConvertFrom-PodeSerializedString { if ($segment.Contains('=')) { $key, $value = $segment -split '=', 2 # Only add values where the key matches the specified KeyName - if ($key -eq $KeyName) { + if ($key -eq $ParameterName) { $values += $value } } @@ -2091,18 +2386,18 @@ function ConvertFrom-PodeSerializedString { return $values } else { - # Remove the leading semicolon (;) prefix from the serialized string - $SerializedString = $SerializedString.TrimStart('?id=') - # For explode=false, split by space (%20) to handle the combined string format - return $SerializedString -split '\|' + # Remove the leading '?id=' prefix from the serialized string + $SerializedInput = $SerializedInput.TrimStart('?id=') + # For explode=false, split by | to handle the combined string format + return $SerializedInput -split '\|' } } 'DeepObject' { - $SerializedString = $SerializedString.TrimStart('?') + $SerializedInput = $SerializedInput.TrimStart('?') # Split the string by '&' to get each key-value pair - $segments = $SerializedString -split '&' + $segments = $SerializedInput -split '&' # Initialize an empty hashtable to store the nested key-value pairs $obj = @{} @@ -2123,7 +2418,7 @@ function ConvertFrom-PodeSerializedString { } # Only process the segment if the main key matches the specified KeyName - if ($mainKey -eq $KeyName) { + if ($mainKey -eq $ParameterName) { # Initialize a reference to the root object $current = $obj @@ -2225,12 +2520,12 @@ function Get-PodePathParameter { [Parameter(ParameterSetName = 'Deserialize')] [string] - $KeyName = 'id' + $ParameterName = 'id' ) if ($WebEvent) { if ($Deserialize.IsPresent) { - return ConvertFrom-PodeSerializedString -SerializedString $WebEvent.Parameters[$Name] -Style $Style -Explode:$Explode -KeyName $KeyName + return ConvertFrom-PodeSerializedString -SerializedInput $WebEvent.Parameters[$Name] -Style $Style -Explode:$Explode -KeyName $ParameterName } return $WebEvent.Parameters[$Name] } @@ -2310,11 +2605,11 @@ function Get-PodeQueryParameter { [Parameter(ParameterSetName = 'Deserialize')] [string] - $KeyName = 'id' + $ParameterName = 'id' ) if ($WebEvent) { if ($Deserialize.IsPresent) { - return ConvertFrom-PodeSerializedString -SerializedString $WebEvent.Query[$Name] -Style $Style -Explode:(!$NoExplode) -KeyName $KeyName + return ConvertFrom-PodeSerializedString -SerializedInput $WebEvent.Query[$Name] -Style $Style -Explode:(!$NoExplode) -KeyName $ParameterName } return $WebEvent.Query[$Name] } @@ -2381,11 +2676,11 @@ function Get-PodeBodyData { [Parameter(ParameterSetName = 'Deserialize')] [string] - $KeyName = 'id' + $ParameterName = 'id' ) if ($WebEvent) { if ($Deserialize.IsPresent) { - return ConvertFrom-PodeSerializedString -SerializedString $WebEvent.Data -Style $Style -Explode:(!$NoExplode) -KeyName $KeyName + return ConvertFrom-PodeSerializedString -SerializedInput $WebEvent.Data -Style $Style -Explode:(!$NoExplode) -KeyName $ParameterName } return $WebEvent.Data } diff --git a/tests/unit/Utility.Tests.ps1 b/tests/unit/Utility.Tests.ps1 index ab9619528..2c9f5d0db 100644 --- a/tests/unit/Utility.Tests.ps1 +++ b/tests/unit/Utility.Tests.ps1 @@ -15,291 +15,335 @@ Describe 'ConvertFrom-PodeSerializedString' { Describe 'Path Parameters' { It 'Convert Simple(Explode) style serialized string to a primitive value' { $serialized = '5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple -Explode - $result | Should -be 5 + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple -Explode + $result | Should -be '5' } It 'Convert Simple(Explode) style serialized string to hashtable' { $serialized = 'role=admin,firstName=Alex' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple -Explode + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple -Explode $expected = @{ role = 'admin' firstName = 'Alex' } - $result.GetEnumerator() | ForEach-Object { - $expected[$_.Key] | Should -Be $_.Value + # Ensure both hashtables have the same number of keys + $result.Keys.Count | Should -Be $expected.Keys.Count + + # Compare values for each key + foreach ($key in $expected.Keys) { + $result[$key] | Should -Be $expected[$key] } } It 'Convert Simple(Explode) style serialized string to array' { $serialized = '3,4,5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple -Explode - $result | Should -be @(3, 4, 5) + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple -Explode + $result | Should -be @('3', '4', '5') } It 'Convert Simple style serialized string to a primitive value' { $serialized = '5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple - $result | Should -be 5 + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple + $result | Should -be '5' } It 'Convert Simple style serialized string to hashtable' { $serialized = 'role,admin,firstName,Alex' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple $expected = @{ role = 'admin' firstName = 'Alex' } - $result.GetEnumerator() | ForEach-Object { - $expected[$_.Key] | Should -Be $_.Value + # Ensure both hashtables have the same number of keys + $result.Keys.Count | Should -Be $expected.Keys.Count + + # Compare values for each key + foreach ($key in $expected.Keys) { + $result[$key] | Should -Be $expected[$key] } } It 'Convert Simple style serialized string to array' { $serialized = '3,4,5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple - $result | Should -be @(3, 4, 5) + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple + $result | Should -be @('3', '4', '5') } It 'Convert Label(Explode) style serialized string to a primitive value' { $serialized = '.5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Label -Explode + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Label -Explode $result | Should -be 5 } It 'Convert Label(Explode) style serialized string to hashtable' { $serialized = '.role=admin.firstName=Alex' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Label -Explode + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Label -Explode $expected = @{ role = 'admin' firstName = 'Alex' } - $result.GetEnumerator() | ForEach-Object { - $expected[$_.Key] | Should -Be $_.Value + # Ensure both hashtables have the same number of keys + $result.Keys.Count | Should -Be $expected.Keys.Count + + # Compare values for each key + foreach ($key in $expected.Keys) { + $result[$key] | Should -Be $expected[$key] } } It 'Convert Label(Explode) style serialized string to array' { $serialized = '.3,4,5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Label -Explode - $result | Should -be @(3, 4, 5) + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Label -Explode + $result | Should -be @('3', '4', '5') } It 'Convert Simple style serialized string to a primitive value' { $serialized = '.5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Label + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Label $result | Should -be 5 } It 'Convert Label style serialized string to hashtable' { $serialized = '.role,admin,firstName,Alex' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Label + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Label $expected = @{ role = 'admin' firstName = 'Alex' } - $result.GetEnumerator() | ForEach-Object { - $expected[$_.Key] | Should -Be $_.Value + # Ensure both hashtables have the same number of keys + $result.Keys.Count | Should -Be $expected.Keys.Count + + # Compare values for each key + foreach ($key in $expected.Keys) { + $result[$key] | Should -Be $expected[$key] } } It 'Convert Label style serialized string to array' { $serialized = '.3,4,5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Label - $result | Should -be @(3, 4, 5) + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Label + $result | Should -be @('3', '4', '5') } It 'Convert Matrix(Explode) style serialized string to a primitive value' { $serialized = ';id=5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix -Explode + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix -Explode $result | Should -be 5 } It 'Convert Matrix(Explode) style serialized string to hashtable' { $serialized = ';role=admin;firstName=Alex' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix -Explode + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix -Explode $expected = @{ role = 'admin' firstName = 'Alex' } - $result.GetEnumerator() | ForEach-Object { - $expected[$_.Key] | Should -Be $_.Value + # Ensure both hashtables have the same number of keys + $result.Keys.Count | Should -Be $expected.Keys.Count + + # Compare values for each key + foreach ($key in $expected.Keys) { + $result[$key] | Should -Be $expected[$key] } } It 'Convert Matrix(Explode) style serialized string to array' { $serialized = ';id=3;id=4;id=5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix -Explode - $result | Should -be @(3, 4, 5) + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix -Explode + $result | Should -be @('3', '4', '5') } It 'Convert Simple style serialized string to a primitive value' { $serialized = ';id=5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix $result | Should -be 5 } It 'Convert Matrix style serialized string to hashtable' { $serialized = ';id=role,admin,firstName,Alex' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix $expected = @{ role = 'admin' firstName = 'Alex' } - $result.GetEnumerator() | ForEach-Object { - $expected[$_.Key] | Should -Be $_.Value + # Ensure both hashtables have the same number of keys + $result.Keys.Count | Should -Be $expected.Keys.Count + + # Compare values for each key + foreach ($key in $expected.Keys) { + $result[$key] | Should -Be $expected[$key] } } It 'Convert Matrix style serialized string to array' { $serialized = ';id=3,4,5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix - $result | Should -be @(3, 4, 5) + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix + $result | Should -be @('3', '4', '5') } It 'Convert Matrix(Explode) style serialized string to a primitive value' { $serialized = ';id=5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix -Explode + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix -Explode $result | Should -be 5 } It 'Convert Matrix(Explode) style serialized string to hashtable' { $serialized = ';role=admin;firstName=Alex' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix -Explode + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix -Explode $expected = @{ role = 'admin' firstName = 'Alex' } - $result.GetEnumerator() | ForEach-Object { - $expected[$_.Key] | Should -Be $_.Value + # Ensure both hashtables have the same number of keys + $result.Keys.Count | Should -Be $expected.Keys.Count + + # Compare values for each key + foreach ($key in $expected.Keys) { + $result[$key] | Should -Be $expected[$key] } } It 'Convert Matrix(Explode) style serialized string to array' { $serialized = ';id=3;id=4;id=5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix -Explode - $result | Should -be @(3, 4, 5) + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix -Explode + $result | Should -be @('3', '4', '5') } It 'Convert Matrix style serialized string to a primitive value' { $serialized = ';id=5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix $result | Should -be 5 } It 'Convert Matrix style serialized string to hashtable' { $serialized = ';id=role,admin,firstName,Alex' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix $expected = @{ role = 'admin' firstName = 'Alex' } - $result.GetEnumerator() | ForEach-Object { - $expected[$_.Key] | Should -Be $_.Value + # Ensure both hashtables have the same number of keys + $result.Keys.Count | Should -Be $expected.Keys.Count + + # Compare values for each key + foreach ($key in $expected.Keys) { + $result[$key] | Should -Be $expected[$key] } } It 'Convert Matrix style serialized string to array' { $serialized = ';id=3,4,5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Matrix - $result | Should -be @(3, 4, 5) + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix + $result | Should -be @('3', '4', '5') } } Describe 'Query Parameters' { It 'Convert Form(Explode) style serialized string to a primitive value' { $serialized = '?id=5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Form -Explode + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form -Explode $result | Should -be 5 } It 'Convert Form(Explode) style serialized string to hashtable' { $serialized = '?role=admin&firstName=Alex' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Form -Explode + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form -Explode $expected = @{ role = 'admin' firstName = 'Alex' } - $result.GetEnumerator() | ForEach-Object { - $expected[$_.Key] | Should -Be $_.Value + # Ensure both hashtables have the same number of keys + $result.Keys.Count | Should -Be $expected.Keys.Count + + # Compare values for each key + foreach ($key in $expected.Keys) { + $result[$key] | Should -Be $expected[$key] } } It 'Convert Form(Explode) style serialized string to array' { $serialized = '?id=3&id=4&id=5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Form -Explode - $result | Should -be @(3, 4, 5) + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form -Explode + $result | Should -be @('3', '4', '5') } It 'Convert Form style serialized string to a primitive value' { $serialized = '?id=5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Form + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form $result | Should -be 5 } It 'Convert Form style serialized string to hashtable' { $serialized = '?id=role,admin,firstName,Alex' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Form + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form $expected = @{ role = 'admin' firstName = 'Alex' } - $result.GetEnumerator() | ForEach-Object { - $expected[$_.Key] | Should -Be $_.Value + # Ensure both hashtables have the same number of keys + $result.Keys.Count | Should -Be $expected.Keys.Count + + # Compare values for each key + foreach ($key in $expected.Keys) { + $result[$key] | Should -Be $expected[$key] } } It 'Convert Form style serialized string to array' { $serialized = '?id=3,4,5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Form - $result | Should -be @(3, 4, 5) + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form + $result | Should -be @('3', '4', '5') } It 'Convert SpaceDelimited(Explode) style serialized string to array' { $serialized = '?id=3&id=4&id=5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style SpaceDelimited -Explode - $result | Should -be @(3, 4, 5) + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style SpaceDelimited -Explode + $result | Should -be @('3', '4', '5') } It 'Convert SpaceDelimited style serialized string to array' { - $serialized = '?id=3%204%205' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style SpaceDelimited - $result | Should -be @(3, 4, 5) + $serialized = '?id=3 4 5' + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style SpaceDelimited + $result | Should -be @('3', '4', '5') } It 'Convert pipeDelimited(Explode) style serialized string to array' { $serialized = '?id=3&id=4&id=5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style pipeDelimited -Explode - $result | Should -be @(3, 4, 5) + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style pipeDelimited -Explode + $result | Should -be @('3', '4', '5') } It 'Convert pipeDelimited style serialized string to array' { $serialized = '?id=3|4|5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style pipeDelimited - $result | Should -be @(3, 4, 5) + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style pipeDelimited + $result | Should -be @('3', '4', '5') } It 'Convert DeepObject(Explode) style serialized string to hashtable' { $serialized = '?id[role]=admin&id[firstName]=Alex' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style DeepObject + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style DeepObject $expected = @{ role = 'admin' firstName = 'Alex' } - $result.GetEnumerator() | ForEach-Object { - $expected[$_.Key] | Should -Be $_.Value + # Ensure both hashtables have the same number of keys + $result.Keys.Count | Should -Be $expected.Keys.Count + + # Compare values for each key + foreach ($key in $expected.Keys) { + $result[$key] | Should -Be $expected[$key] } } It 'Convert DeepObject(Explode) style nested object serialized to hashtable' { $serialized = '?id[role][type]=admin&id[role][level]=high&id[firstName]=Alex' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style DeepObject + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style DeepObject $expected = @{ role = @{ type = 'admin' @@ -319,13 +363,13 @@ Describe 'ConvertFrom-PodeSerializedString' { Describe 'Header Parameters' { It 'Convert Simple(Explode) style serialized string to a primitive value' { $serialized = 'X-MyHeader: 5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple -Explode + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple -Explode $result['X-MyHeader'] | Should -be 5 } It 'Convert Simple(Explode) style serialized string to hashtable' { $serialized = 'X-MyHeader: role=admin,firstName=Alex' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple -Explode + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple -Explode $expected = @{ role = 'admin' firstName = 'Alex' @@ -337,19 +381,19 @@ Describe 'ConvertFrom-PodeSerializedString' { It 'Convert Simple(Explode) style serialized string to array' { $serialized = 'X-MyHeader: 3,4,5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple -Explode - $result['X-MyHeader'] | Should -be @(3, 4, 5) + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple -Explode + $result['X-MyHeader'] | Should -be @('3', '4', '5') } It 'Convert Simple style serialized string to a primitive value' { $serialized = 'X-MyHeader: 5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple $result['X-MyHeader'] | Should -be 5 } It 'Convert Simple style serialized string to hashtable' { $serialized = 'X-MyHeader: role,admin,firstName,Alex' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple $expected = @{ role = 'admin' firstName = 'Alex' @@ -361,27 +405,27 @@ Describe 'ConvertFrom-PodeSerializedString' { It 'Convert Simple style serialized string to array' { $serialized = 'X-MyHeader: 3,4,5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Simple - $result['X-MyHeader'] | Should -be @(3, 4, 5) + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple + $result['X-MyHeader'] | Should -be @('3', '4', '5') } } Describe 'Cookie Parameters' { It 'Convert Form(Explode) style serialized string to a primitive value' { $serialized = 'Cookie: id=5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Form -Explode + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form -Explode $result['Cookie'] | Should -be 5 } It 'Convert Form style serialized string to a primitive value' { $serialized = 'Cookie: id=5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Form + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form $result['Cookie'] | Should -be 5 } It 'Convert Form style serialized string to hashtable' { $serialized = 'Cookie: id=role,admin,firstName,Alex' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Form + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form $expected = @{ role = 'admin' firstName = 'Alex' @@ -393,10 +437,137 @@ Describe 'ConvertFrom-PodeSerializedString' { It 'Convert Form style serialized string to array' { $serialized = 'Cookie: id=3,4,5' - $result = ConvertFrom-PodeSerializedString -SerializedString $serialized -Style Form - $result['Cookie'] | Should -be @(3, 4, 5) + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form + $result['Cookie'] | Should -be @('3', '4', '5') } } + + Describe 'Edge cases' { + + It 'Throws an error for invalid serialization style' { + $serialized = 'some data' + { ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'InvalidStyle' } | Should -Throw + } + + It 'Properly decodes URL-encoded characters' { + $serialized = 'name%3DJohn%20Doe%2Cage%3D30' + + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple -Explode -UrlDecode + + # Define the expected hashtable + $expected = @{ + 'name' = 'John Doe' + 'age' = '30' + } + $result.Keys.Count | Should -Be $expected.Keys.Count + foreach ($key in $expected.Keys) { + $result[$key] | Should -Be $expected[$key] + } + } + + + It 'Handles special characters in keys and values' { + $serialized = 'na!me=Jo@hn,do#e=30$' + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple -Explode + $expected = @{ + 'na!me' = 'Jo@hn' + 'do#e' = '30$' + } + $result.Keys.Count | Should -Be $expected.Keys.Count + foreach ($key in $expected.Keys) { + $result[$key] | Should -Be $expected[$key] + } + } + + It 'Parses deeply nested structures in DeepObject style' { + $serialized = '?user[address][street]=Main St&user[address][city]=Anytown&user[details][age]=30' + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style DeepObject -ParameterName 'user' + $expected = @{ + 'address' = @{ + 'street' = 'Main St' + 'city' = 'Anytown' + } + 'details' = @{ + 'age' = '30' + } + } + # Recursive comparison function + function Compare-Hashtable($expected, $actual) { + $expected.Keys.Count | Should -Be $actual.Keys.Count + foreach ($key in $expected.Keys) { + $actual.ContainsKey($key) | Should -BeTrue -Because "Key '$key' is missing." + if ($expected[$key] -is [hashtable]) { + Compare-Hashtable $expected[$key] $actual[$key] + } + else { + $actual[$key] | Should -Be $expected[$key] + } + } + } + Compare-Hashtable $expected $result + } + + + It 'Handles multiple occurrences of the same parameter in Query style' { + $serialized = '?id=1&id=2&id=3' + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form -Explode -ParameterName 'id' + $result | Should -Be @('1', '2', '3') + } + + It 'Handles single value in SpaceDelimited style without wrapping in an array' { + $serialized = '?id=42' + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style SpaceDelimited -ParameterName 'id' + $result | Should -Be '42' + } + + It 'Parses Matrix style without explode correctly' { + $serialized = ';id=1,2,3' + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix -ParameterName 'id' + $result | Should -Be @('1', '2', '3') + } + It 'Handles missing dot prefix in Label style gracefully' { + $serialized = 'name=value' + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Label -Explode + $expected = @{ 'name' = 'value' } + $result.Keys.Count | Should -Be $expected.Keys.Count + foreach ($key in $expected.Keys) { + $result[$key] | Should -Be $expected[$key] + } + } + + It 'Parses headers with multiple values correctly' { + $serialized = 'X-Custom-Header: value1,value2,value3' + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple + $result['X-Custom-Header'] | Should -Be @('value1', 'value2', 'value3') + } + + It 'return the SerializedString content for malformed input string' { + $serialized = 'name===value' + ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple -Explode | Should -be $serialized + } + + It 'Throws an error for unsupported characters in Style parameter' { + { ConvertFrom-PodeSerializedString -SerializedInput 'data' -Style 'S!mple' } | Should -Throw + } + + It 'Parses complex real-world query strings correctly' { + $serialized = '?filter=name%20eq%20%27John%27&sort=asc&limit=10' + + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form -Explode -UrlDecode + + $expected = @{ + 'filter' = "name eq 'John'" + 'sort' = 'asc' + 'limit' = '10' + } + $result.Keys.Count | Should -Be $expected.Keys.Count + foreach ($key in $expected.Keys) { + $result[$key] | Should -Be $expected[$key] + } + } + + } + } @@ -477,10 +648,10 @@ Describe 'ConvertTo-PodeSerializedString' { anotherName = 'anotherValue' number = 10 } - $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Simple' - $sortedResult = SortSerializedString -SerializedString $result -Delimiter ',' -GroupPairs + $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Simple' + $sortedResult = SortSerializedString -SerializedInput $result -Delimiter ',' -GroupPairs $expected = 'name,value,number,10,anotherName,anotherValue' - $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ',' -GroupPairs + $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter ',' -GroupPairs $sortedResult | Should -Be $sortedExpected } @@ -490,10 +661,10 @@ Describe 'ConvertTo-PodeSerializedString' { anotherName = 'anotherValue' number = 10 } - $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Simple' -Explode - $sortedResult = SortSerializedString -SerializedString $result -Delimiter ',' + $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Simple' -Explode + $sortedResult = SortSerializedString -SerializedInput $result -Delimiter ',' $expected = 'name=value,number=10,anotherName=anotherValue' - $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ',' + $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter ',' $sortedResult | Should -Be $sortedExpected } @@ -503,11 +674,11 @@ Describe 'ConvertTo-PodeSerializedString' { anotherName = 'anotherValue' number = 10 } - $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Label' + $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Label' - $sortedResult = SortSerializedString -SerializedString $result -Delimiter ',' -SkipHead '.' + $sortedResult = SortSerializedString -SerializedInput $result -Delimiter ',' -SkipHead '.' $expected = '.anotherName,anotherValue,number,10,name,value' - $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ',' -SkipHead '.' + $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter ',' -SkipHead '.' $sortedResult | Should -Be $sortedExpected } @@ -517,10 +688,10 @@ Describe 'ConvertTo-PodeSerializedString' { anotherName = 'anotherValue' number = 10 } - $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Label' -Explode - $sortedResult = SortSerializedString -SerializedString $result -Delimiter ',' -SkipHead '.' + $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Label' -Explode + $sortedResult = SortSerializedString -SerializedInput $result -Delimiter ',' -SkipHead '.' $expected = '.anotherName=anotherValue,number=10,name=value' - $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ',' -SkipHead '.' + $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter ',' -SkipHead '.' $sortedResult | Should -Be $sortedExpected } @@ -530,10 +701,10 @@ Describe 'ConvertTo-PodeSerializedString' { anotherName = 'anotherValue' number = 10 } - $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Matrix' - $sortedResult = SortSerializedString -SerializedString $result -Delimiter ',' -GroupPairs -SkipHead ';id=' + $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Matrix' + $sortedResult = SortSerializedString -SerializedInput $result -Delimiter ',' -GroupPairs -SkipHead ';id=' $expected = ';id=name,value,number,10,anotherName,anotherValue' - $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ',' -GroupPairs -SkipHead ';id=' + $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter ',' -GroupPairs -SkipHead ';id=' $sortedResult | Should -Be $sortedExpected } @@ -543,10 +714,10 @@ Describe 'ConvertTo-PodeSerializedString' { anotherName = 'anotherValue' number = 10 } - $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Matrix' -Explode - $sortedResult = SortSerializedString -SerializedString $result -Delimiter ';' -SkipHead ';' + $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Matrix' -Explode + $sortedResult = SortSerializedString -SerializedInput $result -Delimiter ';' -SkipHead ';' $expected = ';name=value;number=10;anotherName=anotherValue' - $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ';' -SkipHead ';' + $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter ';' -SkipHead ';' $sortedResult | Should -Be $sortedExpected } @@ -556,10 +727,10 @@ Describe 'ConvertTo-PodeSerializedString' { anotherName = 'anotherValue' number = 10 } - $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Form' - $sortedResult = SortSerializedString -SerializedString $result -Delimiter ',' -GroupPairs -SkipHead '?id=' + $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Form' + $sortedResult = SortSerializedString -SerializedInput $result -Delimiter ',' -GroupPairs -SkipHead '?id=' $expected = '?id=name,value,number,10,anotherName,anotherValue' - $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter ',' -GroupPairs -SkipHead '?id=' + $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter ',' -GroupPairs -SkipHead '?id=' $sortedResult | Should -Be $sortedExpected } @@ -569,10 +740,10 @@ Describe 'ConvertTo-PodeSerializedString' { anotherName = 'anotherValue' number = 10 } - $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Form' -Explode - $sortedResult = SortSerializedString -SerializedString $result -Delimiter '&' -SkipHead '?' + $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Form' -Explode + $sortedResult = SortSerializedString -SerializedInput $result -Delimiter '&' -SkipHead '?' $expected = '?name=value&number=10&anotherName=anotherValue' - $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter '&' -SkipHead '?' + $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter '&' -SkipHead '?' $sortedResult | Should -Be $sortedExpected } @@ -584,82 +755,150 @@ Describe 'ConvertTo-PodeSerializedString' { anotherName = 'anotherValue' number = 10 } - $result = ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'DeepObject' -Explode - $sortedResult = SortSerializedString -SerializedString $result -Delimiter '&' -SkipHead '?' -RemovePattern 'id\[|\]' + $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'DeepObject' -Explode + $sortedResult = SortSerializedString -SerializedInput $result -Delimiter '&' -SkipHead '?' -RemovePattern 'id\[|\]' $expected = '?id[name]=value&id[number]=10&id[anotherName]=anotherValue' - $sortedExpected = SortSerializedString -SerializedString $expected -Delimiter '&' -SkipHead '?' -RemovePattern 'id\[|\]' + $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter '&' -SkipHead '?' -RemovePattern 'id\[|\]' $sortedResult | Should -Be $sortedExpected } It 'should convert array to Simple style serialized string' { $array = @(3, 4, 5) - $result = $array |ConvertTo-PodeSerializedString -Style 'Simple' + $result = $array | ConvertTo-PodeSerializedString -Style 'Simple' $result | Should -Be '3,4,5' } It 'should convert array to Simple style serialized string with Explode' { $array = @(3, 4, 5) - $result = $array |ConvertTo-PodeSerializedString -Style 'Simple' -Explode + $result = $array | ConvertTo-PodeSerializedString -Style 'Simple' -Explode $result | Should -Be '3,4,5' } It 'should convert array to Label style serialized string' { $array = @(3, 4, 5) - $result = $array |ConvertTo-PodeSerializedString -Style 'Label' + $result = $array | ConvertTo-PodeSerializedString -Style 'Label' $result | Should -Be '.3,4,5' } It 'should convert array to Label style serialized string with Explode' { $array = @(3, 4, 5) - $result = $array |ConvertTo-PodeSerializedString -Style 'Label' -Explode + $result = $array | ConvertTo-PodeSerializedString -Style 'Label' -Explode $result | Should -Be '.3,4,5' } It 'should convert array to Matrix style serialized string' { $array = @(3, 4, 5) - $result = $array |ConvertTo-PodeSerializedString -Style 'Matrix' + $result = $array | ConvertTo-PodeSerializedString -Style 'Matrix' $result | Should -Be ';id=3,4,5' } It 'should convert array to Matrix style serialized string with Explode' { $array = @(3, 4, 5) - $result = $array |ConvertTo-PodeSerializedString -Style 'Matrix' -Explode + $result = $array | ConvertTo-PodeSerializedString -Style 'Matrix' -Explode $result | Should -Be ';id=3;id=4;id=5' } It 'should convert array to SpaceDelimited style serialized string' { $array = @(3, 4, 5) - $result = $array |ConvertTo-PodeSerializedString -Style 'SpaceDelimited' + $result = $array | ConvertTo-PodeSerializedString -Style 'SpaceDelimited' $result | Should -Be '?id=3%204%205' } It 'should convert array to SpaceDelimited style serialized string with Explode' { $array = @(3, 4, 5) - $result = $array |ConvertTo-PodeSerializedString -Style 'SpaceDelimited' -Explode + $result = $array | ConvertTo-PodeSerializedString -Style 'SpaceDelimited' -Explode $result | Should -Be '?id=3&id=4&id=5' } It 'should convert array to PipeDelimited style serialized string' { $array = @(3, 4, 5) - $result = $array |ConvertTo-PodeSerializedString -Style 'PipeDelimited' - $result | Should -Be '?id=3|4|5' + $result = $array | ConvertTo-PodeSerializedString -Style 'PipeDelimited' + $result | Should -Be '?id=3%7C4%7C5' } It 'should convert array to PipeDelimited style serialized string with Explode' { $array = @(3, 4, 5) - $result = $array |ConvertTo-PodeSerializedString -Style 'PipeDelimited' -Explode + $result = $array | ConvertTo-PodeSerializedString -Style 'PipeDelimited' -Explode $result | Should -Be '?id=3&id=4&id=5' } - It 'should throw an error for unsupported serialization type' { + It 'should throw an error for unsupported serialization style' { $hashtable = @{ name = 'value' anotherName = 'anotherValue' number = 10 } - { ConvertTo-PodeSerializedString -Hashtable $hashtable -Style 'Unsupported' } | Should -Throw + { ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Unsupported' } | Should -Throw } + + + It 'should convert array to Matrix style without URL encoding' { + $array = @('value one', 'value/two', 'value&three') + $result = $array | ConvertTo-PodeSerializedString -Style 'Matrix' -NoUrlEncode + $result | Should -Be ';id=value one,value/two,value&three' + } + + It 'should handle special characters with URL encoding' { + $array = @('value one', 'value/two', 'value&three') + $result = $array | ConvertTo-PodeSerializedString -Style 'Matrix' + $result | Should -Be ';id=value%20one,value%2Ftwo,value%26three' + } + + It 'should handle empty array input' { + $array = @() + $result = $array | ConvertTo-PodeSerializedString -Style 'Simple' + $result | Should -Be '' + } + + It 'should handle empty hashtable input by returning an empty string' { + $hashtable = @{} + $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Simple' + $result | Should -Be '' + } + + It 'should use custom parameter name' { + $array = @(3, 4, 5) + $result = $array | ConvertTo-PodeSerializedString -Style 'Matrix' -ParameterName 'customId' + $result | Should -Be ';customId=3,4,5' + } + + It 'should correctly serialize single-element array' { + $array = @('singleValue') + $result = ConvertTo-PodeSerializedString -InputObject $array -Style 'Simple' + $result | Should -Be 'singleValue' + } + + It 'should correctly serialize single-entry hashtable' { + $hashtable = @{ key = 'value' } + $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Form' -Explode + $result | Should -Be '?key=value' + } + + It 'should URL-encode special characters in keys and values' { + $hashtable = @{ + 'name with spaces' = 'value/with/special&chars' + } + $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Form' -Explode + $expected = '?name%20with%20spaces=value%2Fwith%2Fspecial%26chars' + $result | Should -Be $expected + } + It 'should not URL-encode when NoUrlEncode switch is used' { + $hashtable = @{ + 'name with spaces' = 'value/with/special&chars' + } + $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Form' -Explode -NoUrlEncode + $expected = '?name with spaces=value/with/special&chars' + $result | Should -Be $expected + } + + It 'should use custom ParameterName in serialization' { + $array = @(1, 2, 3) + $result = ConvertTo-PodeSerializedString -InputObject $array -Style 'Matrix' -ParameterName 'customParam' + $result | Should -Be ';customParam=1,2,3' + } + + } From 139355864ff674dc59e3157892178f1456bd2202 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sun, 15 Sep 2024 09:23:30 -0700 Subject: [PATCH 6/8] fix a not completed parameter rename --- src/Public/Utilities.ps1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index 546a41fff..22e15b2d6 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -2476,7 +2476,7 @@ function ConvertFrom-PodeSerializedString { Defines the deserialization style to use when interpreting the parameter value. Valid options are 'Simple', 'Label', and 'Matrix'. The default is 'Simple'. Applicable only when the `-Deserialize` switch is used. -.PARAMETER KeyName +.PARAMETER ParameterName Specifies the key name to use when deserializing the parameter value. The default value is 'id'. This option is useful for mapping the parameter data accurately during deserialization. Applicable only when the `-Deserialize` switch is used. @@ -2525,7 +2525,7 @@ function Get-PodePathParameter { ) if ($WebEvent) { if ($Deserialize.IsPresent) { - return ConvertFrom-PodeSerializedString -SerializedInput $WebEvent.Parameters[$Name] -Style $Style -Explode:$Explode -KeyName $ParameterName + return ConvertFrom-PodeSerializedString -SerializedInput $WebEvent.Parameters[$Name] -Style $Style -Explode:$Explode -ParameterName $ParameterName } return $WebEvent.Parameters[$Name] } @@ -2561,7 +2561,7 @@ function Get-PodePathParameter { 'Label', 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. The default is 'Form'. Applicable only when the `-Deserialize` switch is used. -.PARAMETER KeyName +.PARAMETER ParameterName Specifies the key name to use when deserializing the query parameter value. The default value is 'id'. This option is useful for mapping the query parameter data accurately during deserialization. Applicable only when the `-Deserialize` switch is used. @@ -2609,7 +2609,7 @@ function Get-PodeQueryParameter { ) if ($WebEvent) { if ($Deserialize.IsPresent) { - return ConvertFrom-PodeSerializedString -SerializedInput $WebEvent.Query[$Name] -Style $Style -Explode:(!$NoExplode) -KeyName $ParameterName + return ConvertFrom-PodeSerializedString -SerializedInput $WebEvent.Query[$Name] -Style $Style -Explode:(!$NoExplode) -ParameterName $ParameterName } return $WebEvent.Query[$Name] } @@ -2638,7 +2638,7 @@ function Get-PodeQueryParameter { 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. The default is 'Form'. Applicable only when the `-Deserialize` switch is used. -.PARAMETER KeyName +.PARAMETER ParameterName Specifies the key name to use when deserializing the body data. The default value is 'id'. This option is useful for mapping the body data accurately during deserialization. Applicable only when the `-Deserialize` switch is used. @@ -2680,7 +2680,7 @@ function Get-PodeBodyData { ) if ($WebEvent) { if ($Deserialize.IsPresent) { - return ConvertFrom-PodeSerializedString -SerializedInput $WebEvent.Data -Style $Style -Explode:(!$NoExplode) -KeyName $ParameterName + return ConvertFrom-PodeSerializedString -SerializedInput $WebEvent.Data -Style $Style -Explode:(!$NoExplode) -ParameterName $ParameterName } return $WebEvent.Data } From 5c5059a045621f10675da0de774e3e082782a35b Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sun, 20 Oct 2024 21:39:46 -0700 Subject: [PATCH 7/8] Update Utilities.ps1 --- src/Public/Utilities.ps1 | 1376 +------------------------------------- 1 file changed, 29 insertions(+), 1347 deletions(-) diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index 86ea58f0d..4a8c2bd2b 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -2653,1327 +2653,79 @@ function Get-PodePathParameter { } } - -<# -.SYNOPSIS - Retrieves a specific query parameter value from the current Pode web event. - -.DESCRIPTION - The `Get-PodeQueryParameter` function extracts and returns the value of a specified query parameter - from the current Pode web event. This function is designed to access query parameters passed in the URL of a web request, - enabling the handling of incoming data in web applications. - - The function supports deserialization of query parameter values when the `-Deserialize` switch is used, - allowing for interpretation of complex data structures from the query string. - -.PARAMETER Name - The name of the query parameter to retrieve. This parameter is mandatory. - -.PARAMETER Deserialize - Specifies that the query parameter value should be deserialized. When this switch is used, the value will be - interpreted based on the provided style and other deserialization options. - -.PARAMETER NoExplode - Prevents deserialization from exploding arrays in the query parameter value. This is useful when handling - parameters that contain comma-separated values and when array expansion is not desired. Applicable only when - the `-Deserialize` switch is used. - -.PARAMETER Style - Defines the deserialization style to use when interpreting the query parameter value. Valid options are 'Simple', - 'Label', 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. The default is 'Form'. - Applicable only when the `-Deserialize` switch is used. - -.PARAMETER ParameterName - Specifies the key name to use when deserializing the query parameter value. The default value is 'id'. - This option is useful for mapping the query parameter data accurately during deserialization. Applicable only - when the `-Deserialize` switch is used. - -.EXAMPLE - Get-PodeQueryParameter -Name 'userId' - Returns the value of the 'userId' query parameter from the current web event. - -.EXAMPLE - Get-PodeQueryParameter -Name 'filter' -Deserialize -Style 'SpaceDelimited' - Retrieves and deserializes the value of the 'filter' query parameter, using the 'SpaceDelimited' style. - -.EXAMPLE - Get-PodeQueryParameter -Name 'data' -Deserialize -NoExplode - Deserializes the 'data' query parameter value without exploding arrays. - -.NOTES - This function should be used within a route's script block in a Pode server. The `-Deserialize` switch enables - advanced handling of complex query parameter data structures. -#> -function Get-PodeQueryParameter { - [CmdletBinding(DefaultParameterSetName = 'BuiltIn' )] - param( - [Parameter(Mandatory, ParameterSetName = 'Deserialize')] - [Parameter(Mandatory, ParameterSetName = 'BuiltIn')] - [string] - $Name, - - [Parameter(Mandatory = $true, ParameterSetName = 'Deserialize')] - [switch] - $Deserialize, - - [Parameter(ParameterSetName = 'Deserialize')] - [switch] - $NoExplode, - - [Parameter(ParameterSetName = 'Deserialize')] - [ValidateSet('Simple', 'Label', 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' )] - [string] - $Style = 'Form', - - [Parameter(ParameterSetName = 'Deserialize')] - [string] - $ParameterName = 'id' - ) - if ($WebEvent) { - if ($Deserialize.IsPresent) { - return ConvertFrom-PodeSerializedString -SerializedInput $WebEvent.Query[$Name] -Style $Style -Explode:(!$NoExplode) -ParameterName $ParameterName - } - return $WebEvent.Query[$Name] - } -} - -<# -.SYNOPSIS - Retrieves the body data from the current Pode web event. - -.DESCRIPTION - The `Get-PodeBodyData` function extracts and returns the body data of the current Pode web event. - This function is designed to access the main content sent in web requests, including methods such as PUT, POST, or any other HTTP methods that support a request body. - It also supports deserialization of the body data, allowing for the interpretation of serialized content. - -.PARAMETER Deserialize - Specifies that the body data should be deserialized. When this switch is used, the body data will be interpreted - based on the provided style and other deserialization options. - -.PARAMETER NoExplode - Prevents deserialization from exploding arrays in the body data. This is useful when handling parameters that - contain comma-separated values and when array expansion is not desired. Applicable only when the `-Deserialize` - switch is used. - -.PARAMETER Style - Defines the deserialization style to use when interpreting the body data. Valid options are 'Simple', 'Label', - 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. The default is 'Form'. Applicable only - when the `-Deserialize` switch is used. - -.PARAMETER ParameterName - Specifies the key name to use when deserializing the body data. The default value is 'id'. This option is useful - for mapping the body data accurately during deserialization. Applicable only when the `-Deserialize` switch is used. - -.EXAMPLE - Get-PodeBodyData - Returns the body data of the current web event. - -.EXAMPLE - Get-PodeBodyData -Deserialize -Style 'Matrix' - Retrieves and deserializes the body data using the 'Matrix' style. - -.EXAMPLE - Get-PodeBodyData -Deserialize -NoExplode - Deserializes the body data without exploding arrays. - -.NOTES - This function should be used within a route's script block in a Pode server. The `-Deserialize` switch enables - advanced handling of complex body data structures. -#> -function Get-PodeBodyData { - [CmdletBinding(DefaultParameterSetName = 'BuiltIn' )] - param( - [Parameter(Mandatory = $true, ParameterSetName = 'Deserialize')] - [switch] - $Deserialize, - - [Parameter(ParameterSetName = 'Deserialize')] - [switch] - $NoExplode, - - [Parameter(ParameterSetName = 'Deserialize')] - [ValidateSet('Simple', 'Label', 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject')] - [string] - $Style = 'Form', - - [Parameter(ParameterSetName = 'Deserialize')] - [string] - $ParameterName = 'id' - ) - if ($WebEvent) { - if ($Deserialize.IsPresent) { - return ConvertFrom-PodeSerializedString -SerializedInput $WebEvent.Data -Style $Style -Explode:(!$NoExplode) -ParameterName $ParameterName - } - return $WebEvent.Data - } -} -<# -.SYNOPSIS - Converts an object (hashtable or array) to a serialized string using a specified serialization style. - -.DESCRIPTION - The `ConvertTo-PodeSerializedString` function takes a hashtable or array and converts it into a serialized string - according to the specified serialization style. It supports various styles such as 'Simple', 'Label', 'Matrix', - 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. - - By default, parameter names and values are URL-encoded to ensure safe inclusion in URLs. You can disable URL encoding - by using the `-NoUrlEncode` switch. - - An optional `-Explode` switch can be used to modify the serialization format for certain styles, altering how arrays - and objects are represented in the serialized string. - -.PARAMETER InputObject - The object to be serialized. This can be a hashtable (or ordered dictionary) or an array. Supports pipeline input. - -.PARAMETER Style - The serialization style to use. Valid values are 'Simple', 'Label', 'Matrix', 'Form', 'SpaceDelimited', - 'PipeDelimited', and 'DeepObject'. Defaults to 'Simple'. - -.PARAMETER Explode - An optional switch to modify the serialization format for certain styles. When used, arrays and objects are - serialized in an expanded form. - -.PARAMETER NoUrlEncode - An optional switch to disable URL encoding of the serialized output. By default, parameter names and values are - URL-encoded individually. Use this switch if you require the output without URL encoding. - -.PARAMETER ParameterName - Specifies the name of the parameter to use in the serialized output. Defaults to 'id' if not specified. - -.EXAMPLE - $item = @{ - name = 'value' - anotherName = 'anotherValue' - } - $serialized = ConvertTo-PodeSerializedString -InputObject $item -Style 'Form' - Write-Output $serialized - - # Output: - # ?id=name%2Cvalue%2CanotherName%2CanotherValue - -.EXAMPLE - $item = @{ - name = 'value' - anotherName = 'anotherValue' - } - $serializedExplode = ConvertTo-PodeSerializedString -InputObject $item -Style 'DeepObject' -Explode - Write-Output $serializedExplode - - # Output: - # ?id[name]=value&id[anotherName]=anotherValue - -.EXAMPLE - $array = @('3', '4', '5') - $serialized = ConvertTo-PodeSerializedString -InputObject $array -Style 'SpaceDelimited' -Explode - Write-Output $serialized - - # Output: - # ?id=3&id=4&id=5 - -.EXAMPLE - $array = @('3', '4', '5') - $serialized = ConvertTo-PodeSerializedString -InputObject $array -Style 'SpaceDelimited' -NoUrlEncode - Write-Output $serialized - - # Output: - # ?id=3 4 5 - -.EXAMPLE - $item = @{ - 'user name' = 'Alice & Bob' - 'role' = 'Admin/User' - } - $serialized = ConvertTo-PodeSerializedString -InputObject $item -Style 'Form' -ParameterName 'account' -NoUrlEncode - Write-Output $serialized - - # Output: - # ?account=user name,Alice & Bob,role,Admin/User - -.NOTES - - 'SpaceDelimited' and 'PipeDelimited' styles for hashtables are not implemented as they are not defined by RFC 6570. - - The 'Form' style with 'Explode' for arrays is not implemented for the same reason. - - The 'Explode' option for 'SpaceDelimited' and 'PipeDelimited' styles for arrays is implemented as per the OpenAPI Specification. - - Additional information regarding serialization: - - OpenAPI Specification Serialization: https://swagger.io/docs/specification/serialization/ - - RFC 6570 - URI Template: https://tools.ietf.org/html/rfc6570 -#> -function ConvertTo-PodeSerializedString { - param ( - [Parameter(Mandatory, ValueFromPipeline = $true, Position = 0)] - [psobject[]] - $InputObject, - - [Parameter()] - [ValidateSet('Simple', 'Label', 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject')] - [string] - $Style = 'Simple', - - [Parameter()] - [switch] - $Explode, - - [Parameter()] - [switch] - $NoUrlEncode, - - [Parameter()] - [string] - $ParameterName = 'id' # Default parameter name - ) - - begin { - # Initialize an array to collect pipeline input - $pipelineValue = @() - } - - process { - # Collect each input object from the pipeline - $pipelineValue += $_ - } - - end { - # Determine if multiple objects were provided via pipeline - if ($pipelineValue.Count -gt 1) { - $inputObjects = $pipelineValue - } - else { - $inputObjects = $InputObject - } - - # Initialize an array to store the serialized strings - $serializedArray = @() - - # return '' if the inputObjects is null - if($null -eq $inputObjects){ - return '' - } - - # Check if there are input objects to process - if ( $inputObjects.Count -gt 0) { - - # Check if the first input object is a hashtable or ordered dictionary - if ($inputObjects[0] -is [hashtable] -or $inputObjects[0] -is [System.Collections.Specialized.OrderedDictionary]) { - - # Process each hashtable item - foreach ($item in $inputObjects) { - switch ($Style) { - - 'Simple' { - # Handle 'Simple' style for hashtables - if ($Explode) { - # Serialize each key-value pair with '=' and join with ',' - $serializedArray += ( ($item.Keys | ForEach-Object { - $key = $_ - $value = $item[$_] - # URL-encode unless $NoUrlEncode is specified - if (-not $NoUrlEncode) { - $key = [uri]::EscapeDataString($key) - $value = [uri]::EscapeDataString($value) - } - "$key=$value" - }) -join ',' ) - } - else { - # Serialize each key-value pair with ',' and join with ',' - $serializedArray += ( ($item.Keys | ForEach-Object { - $key = $_ - $value = $item[$_] - if (-not $NoUrlEncode) { - $key = [uri]::EscapeDataString($key) - $value = [uri]::EscapeDataString($value) - } - "$key,$value" - }) -join ',' ) - } - break - } - - 'Label' { - # Handle 'Label' style for hashtables - if ($Explode) { - # Prepend '.' and serialize each key-value pair with '=' - $serializedArray += '.' + ( ($item.Keys | ForEach-Object { - $key = $_ - $value = $item[$_] - if (-not $NoUrlEncode) { - $key = [uri]::EscapeDataString($key) - $value = [uri]::EscapeDataString($value) - } - "$key=$value" - }) -join ',' ) - } - else { - # Prepend '.' and serialize each key-value pair with ',' - $serializedArray += '.' + ( ($item.Keys | ForEach-Object { - $key = $_ - $value = $item[$_] - if (-not $NoUrlEncode) { - $key = [uri]::EscapeDataString($key) - $value = [uri]::EscapeDataString($value) - } - "$key,$value" - }) -join ',' ) - } - break - } - - 'Matrix' { - # Handle 'Matrix' style for hashtables - if ($Explode) { - # Serialize each key-value pair with ';' prefix - $serializedArray += ( ($item.Keys | ForEach-Object { - $key = $_ - $value = $item[$_] - if (-not $NoUrlEncode) { - $key = [uri]::EscapeDataString($key) - $value = [uri]::EscapeDataString($value) - } - ";$key=$value" - }) -join '' ) - } - else { - # Serialize key-value pairs into a single parameter - $valueString = ( ($item.Keys | ForEach-Object { - $key = $_ - $value = $item[$_] - if (-not $NoUrlEncode) { - $key = [uri]::EscapeDataString($key) - $value = [uri]::EscapeDataString($value) - } - "$key,$value" - }) -join ',' ) - # Encode parameter name if necessary - if (-not $NoUrlEncode) { - $parameterName = [uri]::EscapeDataString($ParameterName) - } - else { - $parameterName = $ParameterName - } - $serializedArray += ";$parameterName=$valueString" - } - break - } - - 'Form' { - # Handle 'Form' style for hashtables - if ($Explode) { - # Serialize each key-value pair as query parameters - $serializedArray += '?' + ( ($item.Keys | ForEach-Object { - $key = $_ - $value = $item[$_] - if (-not $NoUrlEncode) { - $key = [uri]::EscapeDataString($key) - $value = [uri]::EscapeDataString($value) - } - "$key=$value" - }) -join '&' ) - } - else { - # Serialize key-value pairs into a single query parameter - $valueString = ( ($item.Keys | ForEach-Object { - $key = $_ - $value = $item[$_] - if (-not $NoUrlEncode) { - $key = [uri]::EscapeDataString($key) - $value = [uri]::EscapeDataString($value) - } - "$key,$value" - }) -join ',' ) - if (-not $NoUrlEncode) { - $parameterName = [uri]::EscapeDataString($ParameterName) - } - else { - $parameterName = $ParameterName - } - $serializedArray += "?$parameterName=$valueString" - } - break - } - - 'DeepObject' { - # Handle 'DeepObject' style for hashtables - # Encode parameter name once outside the loop - if (-not $NoUrlEncode) { - $parameterNameEncoded = [uri]::EscapeDataString($ParameterName) - } - else { - $parameterNameEncoded = $ParameterName - } - # Serialize each key-value pair using bracket notation - $serializedArray += '?' + ( ($item.Keys | ForEach-Object { - $key = $_ - $value = $item[$_] - if (-not $NoUrlEncode) { - $key = [uri]::EscapeDataString($key) - $value = [uri]::EscapeDataString($value) - } - "$parameterNameEncoded`[$key`]=$value" - }) -join '&' ) - break - } - - # Styles not defined for hashtables - 'SpaceDelimited' { - $serializedArray += '' - Write-Verbose "Serialization for objects using '$Style' style is not defined by RFC 6570." - } - - 'PipeDelimited' { - $serializedArray += '' - Write-Verbose "Serialization for objects using '$Style' style is not defined by RFC 6570." - } - } - } - } - else { - # Process input as an array - switch ($Style) { - - 'Simple' { - # Handle 'Simple' style for arrays - # Both 'Explode' and non-'Explode' result in the same output - $serializedArray += ( ($inputObjects | ForEach-Object { - $value = $_ - if (-not $NoUrlEncode) { - $value = [uri]::EscapeDataString($value) - } - $value - }) -join ',' ) - break - } - - 'Label' { - # Handle 'Label' style for arrays - $serializedArray += '.' + ( ($inputObjects | ForEach-Object { - $value = $_ - if (-not $NoUrlEncode) { - $value = [uri]::EscapeDataString($value) - } - $value - }) -join ',' ) - break - } - - 'Matrix' { - # Handle 'Matrix' style for arrays - if (-not $NoUrlEncode) { - $parameterName = [uri]::EscapeDataString($ParameterName) - } - else { - $parameterName = $ParameterName - } - if ($Explode) { - # Serialize each value with parameter name - $serializedArray += ';' + ( ($inputObjects | ForEach-Object { - $value = $_ - if (-not $NoUrlEncode) { - $value = [uri]::EscapeDataString($value) - } - "$parameterName=$value" - }) -join ';' ) - } - else { - # Serialize values into a single parameter - $valueString = ( ($inputObjects | ForEach-Object { - $value = $_ - if (-not $NoUrlEncode) { - $value = [uri]::EscapeDataString($value) - } - $value - }) -join ',' ) - $serializedArray += ";$parameterName=$valueString" - } - break - } - - 'SpaceDelimited' { - # Handle 'SpaceDelimited' style for arrays - if (-not $NoUrlEncode) { - $parameterName = [uri]::EscapeDataString($ParameterName) - } - else { - $parameterName = $ParameterName - } - if ($Explode) { - # Serialize each value as a separate parameter - $valueStrings = $inputObjects | ForEach-Object { - $value = $_ - if (-not $NoUrlEncode) { - $value = [uri]::EscapeDataString($value) - } - "$parameterName=$value" - } - $serializedArray += '?' + ($valueStrings -join '&') - } - else { - # Join values with a space - $valueString = ($inputObjects -join ' ') - if (-not $NoUrlEncode) { - $valueString = [uri]::EscapeDataString($valueString) - } - $serializedArray += "?$parameterName=$valueString" - } - break - } - - 'PipeDelimited' { - # Handle 'PipeDelimited' style for arrays - if (-not $NoUrlEncode) { - $parameterName = [uri]::EscapeDataString($ParameterName) - } - else { - $parameterName = $ParameterName - } - if ($Explode) { - # Serialize each value as a separate parameter - $valueStrings = $inputObjects | ForEach-Object { - $value = $_ - if (-not $NoUrlEncode) { - $value = [uri]::EscapeDataString($value) - } - "$parameterName=$value" - } - $serializedArray += '?' + ($valueStrings -join '&') - } - else { - # Join values with a pipe '|' - $valueString = ($inputObjects -join '|') - if (-not $NoUrlEncode) { - $valueString = [uri]::EscapeDataString($valueString) - } - $serializedArray += "?$parameterName=$valueString" - } - break - } - - 'Form' { - # Handle 'Form' style for arrays - if (-not $NoUrlEncode) { - $parameterName = [uri]::EscapeDataString($ParameterName) - } - else { - $parameterName = $ParameterName - } - if ($Explode) { - # 'Explode' is not defined for arrays in 'Form' style - $serializedArray += '' - Write-Verbose "Serialization for array using '$Style' style with 'Explode' is not defined by RFC 6570." - } - else { - # Serialize values into a single parameter - $valueString = ( ($inputObjects | ForEach-Object { - $value = $_ - if (-not $NoUrlEncode) { - $value = [uri]::EscapeDataString($value) - } - $value - }) -join ',' ) - $serializedArray += "$parameterName=$valueString" - } - break - } - - # 'DeepObject' is not defined for arrays - 'DeepObject' { - $serializedArray += '' - Write-Verbose "Serialization for arrays using '$Style' style is not defined by RFC 6570." - } - } - } - } - - # Return the serialized string(s) - return $serializedArray - } -} - - -<# -.SYNOPSIS - Converts a serialized string back into its original data structure based on the specified serialization style. - -.DESCRIPTION - The `ConvertFrom-PodeSerializedString` function takes a serialized string and converts it back into its original data structure (e.g., hashtable, array). - The function requires the serialization style to be specified via the `-Style` parameter. - Supported styles are 'Simple', 'Label', 'Matrix', 'Query', 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. - The function also accepts an optional `-Explode` switch to indicate whether the string uses exploded serialization. - The `-ParameterName` parameter can be used to specify the key name when processing certain styles, such as 'Matrix' and 'DeepObject'. - -.PARAMETER SerializedInput - The serialized string to be converted back into its original data structure. - -.PARAMETER Style - The serialization style to use for deserialization. Options are 'Simple', 'Label', 'Matrix', 'Query', 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. The default is 'Form'. - -.PARAMETER Explode - Indicates whether the string uses exploded serialization (`-Explode`) or not (omit `-Explode`). This affects how arrays and objects are handled. - -.PARAMETER ParameterName - Specifies the key name to match when processing certain styles, such as 'Matrix' and 'DeepObject'. The default is 'id'. - -.PARAMETER UrlDecode - If specified, the function will decode the input string using URL decoding before processing it. This is useful - for handling serialized inputs that include URL-encoded characters, such as `%20` for spaces. - -.EXAMPLE - # Simple style, explode = true - $serialized = "name=value,anotherName=anotherValue" - $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Simple' -Explode - Write-Output $result - -.EXAMPLE - # Simple style, explode = false - $serialized = "name,value,anotherName,anotherValue" - $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Simple' - Write-Output $result - -.EXAMPLE - # Label style, explode = true - $serialized = ".name=value.anotherName=anotherValue" - $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Label' -Explode - Write-Output $result - -.EXAMPLE - # Label style, explode = false - $serialized = ".name,value,anotherName,anotherValue" - $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Label' - Write-Output $result - -.EXAMPLE - # Matrix style, explode = true - $serialized = ";name=value;anotherName=anotherValue" - $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Matrix' -Explode - Write-Output $result - -.EXAMPLE - # Matrix style, explode = false - $serialized = ";id=3,4,5" - $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Matrix' -ParameterName 'id' - Write-Output $result - -.EXAMPLE - # Query style, explode = true - $serialized = "?name=value&anotherName=anotherValue" - $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Query' -Explode - Write-Output $result - -.EXAMPLE - # Query style, explode = false - $serialized = "?name,value,anotherName,anotherValue" - $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Query' - Write-Output $result - -.EXAMPLE - # Form style, explode = true - $serialized = "?name=value&anotherName=anotherValue" - $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Form' -Explode - Write-Output $result - -.EXAMPLE - # Form style, explode = false - $serialized = "?name,value,anotherName,anotherValue" - $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Form' - Write-Output $result - -.EXAMPLE - # SpaceDelimited style, explode = true - $serialized = "?id=3&id=4&id=5" - $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'SpaceDelimited' -Explode -ParameterName 'id' - Write-Output $result - -.EXAMPLE - # SpaceDelimited style, explode = false - $serialized = "?id=3%204%205" - $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'SpaceDelimited' -ParameterName 'id' - Write-Output $result - -.EXAMPLE - # PipeDelimited style, explode = true - $serialized = "?id=3&id=4&id=5" - $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'PipeDelimited' -Explode -ParameterName 'id' - Write-Output $result - -.EXAMPLE - # PipeDelimited style, explode = false - $serialized = "?id=3|4|5" - $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'PipeDelimited' -ParameterName 'id' - Write-Output $result - -.EXAMPLE - # DeepObject style - $serialized = "myId[role]=admin&myId[firstName]=Alex" - $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'DeepObject' -ParameterName 'myId' - Write-Output $result - -.NOTES - For more information on serialization styles, refer to: - - https://swagger.io/docs/specification/serialization/ - - https://tools.ietf.org/html/rfc6570 -#> - -function ConvertFrom-PodeSerializedString { - param ( - [Parameter(Mandatory, ValueFromPipeline = $true, Position = 0)] - [string] $SerializedInput, - - [Parameter()] - [ValidateSet('Simple', 'Label', 'Matrix', 'Query', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' )] - [string] - $Style = 'Form', - - [Parameter()] - [switch] - $Explode, - - [Parameter()] - [string] - $ParameterName = 'id', # Default key name if not specified - - [Parameter()] - [switch] - $UrlDecode - ) - - process { - if($UrlDecode){ - $SerializedInput = [System.Web.HttpUtility]::UrlDecode($SerializedInput) - } - # Main deserialization logic based on style - switch ($Style) { - 'Simple' { - # Check for header pattern and extract it if present - if ($SerializedInput -match '^([a-zA-Z0-9_-]+):') { - # Extract the variable name and strip it from the serialized string - $headerName = $matches[1] - $SerializedInput = ($SerializedInput -replace "^$($headerName):", '').Trim() - } - - $segments = $SerializedInput -split ',' - - # If there's only one segment, return it directly - if ($segments.Count -eq 1) { - $result = $segments[0] - } - else { - if ($Explode) { - # Handling explode=true case - - # Check if the number of '=' is equal to the count of segments - if ((($SerializedInput -split '=').Count - 1) -eq $segments.Count) { - $obj = @{} - foreach ($pair in $segments) { - if ($pair.Contains('=')) { - $key, $value = $pair -split '=', 2 # Split into exactly two parts - $obj[$key] = $value - } - } - $result = $obj - } - else { - # Return as an array if the explode conditions don't match - $result = $segments - } - } - else { - # Handling explode=false case - - # Check if it's likely an object by checking if the count of segments is even - if ($segments.Count % 2 -eq 0) { - # Try to parse as an object - $obj = @{} - for ($i = 0; $i -lt $segments.Count; $i += 2) { - $key = $segments[$i] - # Validate the key format - if ($key -match '^[a-zA-Z_][a-zA-Z0-9_]*$') { - $obj[$key] = $segments[$i + 1] - } - else { - # If the key is invalid, return the original segments as an array - $result = $segments - } - } - # Return the object if all keys are valid - $result = $obj - } - else { - # If not an object, treat it as an array - $result = $segments - } - } - } - - if ($headerName) { - return @{$headerName = $result } - } - else { - return $result - } - - } - 'Label' { - # Remove the leading dot (.) prefix from the serialized string - $SerializedInput = $SerializedInput.TrimStart('.') - - # Split the string by dot - $segments = $SerializedInput -split '\.' - - # Handle the explode=true case - if ($Explode) { - # Handling explode=true: each segment is a key=value pair - $obj = @{} - foreach ($segment in $segments) { - if ($segment.Contains('=')) { - $key, $value = $segment -split '=', 2 # Split into exactly two parts - $obj[$key] = $value - } - else { - # If a segment does not contain '=', treat it as an array element - return $segments -split ',' - } - } - return $obj - } - else { - # Handling explode=false: all segments form a combined structure - # Split the string by commas within each segment - $combinedSegments = ($SerializedInput -split ',') - - # Check if it's likely an object by checking if the count is even - if ($combinedSegments.Count % 2 -eq 0) { - # Try to parse as an object - $obj = @{} - for ($i = 0; $i -lt $combinedSegments.Count; $i += 2) { - $key = $combinedSegments[$i] - - # Validate if the key is a suitable key - if ($key -match '^[a-zA-Z_][a-zA-Z0-9_]*$') { - $value = $combinedSegments[$i + 1] - $obj[$key] = $value - } - else { - # If validation fails, return segments as array - return $combinedSegments - } - } - return $obj - } - - # If not an object, return as an array - return $combinedSegments - } - } - 'Matrix' { - # Handle the explode=true case - if ($Explode) { - # Remove the leading semicolon (;) prefix from the serialized string - $SerializedInput = $SerializedInput.TrimStart(';') - - # Split by semicolon to get segments - $segments = $SerializedInput -split ';' - - # If each segment doesn't contain '=', treat it as an array - if ($segments -notmatch '=') { - # Return as an array of individual elements split by commas - return $segments -split ',' - } - - # Initialize an empty hashtable to store key-value pairs - $obj = @{} - $values = @() - - - foreach ($segment in $segments) { - if ($segment.Contains('=')) { - $key, $value = $segment -split '=', 2 - - # If the key matches the specified key name - if ($key -eq $ParameterName) { - $values += $value - } - else { - # If a key doesn't match, treat as a normal key-value pair in the hashtable - $obj[$key] = $value - } - } - } - - # If all segments matched the specified key name, return the values as an array - if ($values.Count -eq $segments.Count) { - if ($values.Count -eq 1) { - return $values[0] - } - return $values - } - - # Merge values back into the object if any key matches the KeyName - if ($values.Count -gt 0) { - $obj[$ParameterName] = if ($values.Count -eq 1) { $values[0] } else { $values } - } - - # Return the hashtable if it contains any key-value pairs - if ($obj.Count -gt 0) { - return $obj - } - else { - return $values - } - } - else { - # Handling explode=false: - - # Remove the leading semicolon (;) prefix from the serialized string - $SerializedInput = $SerializedInput.TrimStart(";$ParameterName=") - - # Split by semicolon to get segments - $segments = $SerializedInput -split ',' - - # If there's only one segment, return it directly - if ($segments.Count -eq 1) { - return $segments[0] - } - - # Check if it's likely an object by checking if the count of segments is even - if ($segments.Count % 2 -eq 0) { - # Try to parse as an object - $obj = @{} - for ($i = 0; $i -lt $segments.Count; $i += 2) { - $key = $segments[$i] - # Validate the key format - if ($key -match '^[a-zA-Z_][a-zA-Z0-9_]*$') { - $obj[$key] = $segments[$i + 1] - } - else { - # If the key is invalid, return the original segments as an array - return $segments - } - } - # Return the object if all keys are valid - return $obj - } - - # If not an object, treat it as an array - return $segments - } - } - - 'Form' { - # Check for header pattern and extract it if present - if ($SerializedInput -match '^([a-zA-Z0-9_-]+):') { - # Extract the variable name and strip it from the serialized string - $headerName = $matches[1] - $SerializedInput = ($SerializedInput -replace "^$($headerName):", '').Trim().TrimStart("$ParameterName=") - } - else { - if ($Explode) { - # Remove the leading semicolon (;) prefix from the serialized string - $SerializedInput = $SerializedInput.TrimStart('?') - } - else { - # Remove the leading semicolon (;) prefix from the serialized string - $SerializedInput = $SerializedInput.TrimStart("?$ParameterName=") - } - } - - # Handle the explode=true case - if ($Explode) { - # Split by semicolon to get segments - $segments = $SerializedInput -split '&' - - # If each segment doesn't contain '=', treat it as an array - if ($segments -notmatch '=') { - # Return as an array of individual elements split by commas - $result = $segments -split ',' - } - else { - # Initialize an empty hashtable to store key-value pairs - $obj = @{} - $values = @() - - foreach ($segment in $segments) { - if ($segment.Contains('=')) { - $key, $value = $segment -split '=', 2 - - # If the key matches the specified key name - if ($key -eq $ParameterName) { - $values += $value - } - else { - # If a key doesn't match, treat as a normal key-value pair in the hashtable - $obj[$key] = $value - } - } - } - - # If all segments matched the specified key name, return the values as an array - if ($values.Count -eq $segments.Count) { - if ($values.Count -eq 1) { - $result = $values[0] - } - else { - $result = $values - } - } - else { - - # Merge values back into the object if any key matches the KeyName - if ($values.Count -gt 0) { - $obj[$ParameterName] = if ($values.Count -eq 1) { $values[0] } else { $values } - } - - # Return the hashtable if it contains any key-value pairs - if ($obj.Count -gt 0) { - return $obj - } - else { - return $values - } - } - } - } - else { - # Handling explode=false - - # Split by semicolon to get segments - $segments = $SerializedInput -split ',' - - # If there's only one segment, return it directly - if ($segments.Count -eq 1) { - $result = $segments[0] - } - # Check if it's likely an object by checking if the count of segments is even - elseif ($segments.Count % 2 -eq 0) { - # Try to parse as an object - $obj = @{} - for ($i = 0; $i -lt $segments.Count; $i += 2) { - $key = $segments[$i] - # Validate the key format - if ($key -match '^[a-zA-Z_][a-zA-Z0-9_]*$') { - $obj[$key] = $segments[$i + 1] - } - else { - # If the key is invalid, return the original segments as an array - $result = $segments - break - } - } - if (!$result) { - # Return the object if all keys are valid - $result = $obj - } - } - else { - # If not an object, treat it as an array - $result = $segments - } - - } - - if ($headerName) { - return @{$headerName = $result } - } - else { - return $result - } - } - - 'SpaceDelimited' { - if ($Explode) { - # Remove the leading semicolon (;) prefix from the serialized string - $SerializedInput = $SerializedInput.TrimStart('?') - - # For explode=true, split by '&' to treat each value as a separate occurrence - $segments = $SerializedInput -split '&' - - # Initialize an array to store values that match the specified KeyName - $values = @() - foreach ($segment in $segments) { - if ($segment.Contains('=')) { - $key, $value = $segment -split '=', 2 - # Only add values where the key matches the specified KeyName - if ($key -eq $ParameterName) { - $values += $value - } - } - } - # Return the array of values that matched the KeyName - return $values - } - else { - # Remove the leading semicolon '?id=' prefix from the serialized string - $SerializedInput = $SerializedInput.TrimStart('?id=') - # For explode=false, split by space (%20) to handle the combined string format - return $SerializedInput -split ' ' - } - } - - 'PipeDelimited' { - if ($Explode) { - $SerializedInput = $SerializedInput.TrimStart('?') - # For explode=true, split by '&' to treat each value as a separate occurrence - $segments = $SerializedInput -split '&' - - # Initialize an array to store values that match the specified KeyName - $values = @() - foreach ($segment in $segments) { - if ($segment.Contains('=')) { - $key, $value = $segment -split '=', 2 - # Only add values where the key matches the specified KeyName - if ($key -eq $ParameterName) { - $values += $value - } - } - } - # Return the array of values that matched the KeyName - return $values - } - else { - # Remove the leading '?id=' prefix from the serialized string - $SerializedInput = $SerializedInput.TrimStart('?id=') - # For explode=false, split by | to handle the combined string format - return $SerializedInput -split '\|' - } - } - - 'DeepObject' { - $SerializedInput = $SerializedInput.TrimStart('?') - - # Split the string by '&' to get each key-value pair - $segments = $SerializedInput -split '&' - - # Initialize an empty hashtable to store the nested key-value pairs - $obj = @{} - foreach ($segment in $segments) { - if ($segment.Contains('=')) { - # Split each segment by '=' into key and value - $key, $value = $segment -split '=', 2 - - # Extract the main key and nested keys using regex - $allMatches = [regex]::Matches($key, '([^\[\]]+)') - - # Extract the main key (first match) and remaining nested keys - $mainKey = $allMatches[0].Groups[1].Value - # Manually extract remaining nested keys as a list of strings - $nestedKeys = @() - for ($i = 1; $i -lt $allMatches.Count; $i++) { - $nestedKeys += $allMatches[$i].Groups[1].Value - } - - # Only process the segment if the main key matches the specified KeyName - if ($mainKey -eq $ParameterName) { - # Initialize a reference to the root object - $current = $obj - - # Iterate over the nested keys to build the structure - foreach ($nestedKey in $nestedKeys) { - # If this is the last key, assign the value - if ($nestedKey -eq $nestedKeys[-1]) { - $current[$nestedKey] = $value - } - else { - # Create a new hashtable if the nested key doesn't exist - if (-not $current.ContainsKey($nestedKey)) { - $current[$nestedKey] = @{} - } - # Move deeper into the nested structure - $current = $current[$nestedKey] - } - } - } - } - } - - # Return the constructed hashtable with nested keys and values - return $obj - } - - } - } -} - <# .SYNOPSIS - Retrieves a specific parameter value from the current Pode web event. + Retrieves the body data from the current Pode web event. .DESCRIPTION - The `Get-PodePathParameter` function extracts and returns the value of a specified parameter - from the current Pode web event. This function can access parameters passed in the URL path, query string, - or body of a web request, making it useful in web applications to dynamically handle incoming data. - - The function supports deserialization of parameter values when the `-Deserialize` switch is used. - This allows for interpreting serialized data structures, like arrays or complex objects, from the web request. - -.PARAMETER Name - The name of the parameter to retrieve. This parameter is mandatory. + The `Get-PodeBodyData` function extracts and returns the body data of the current Pode web event. + This function is designed to access the main content sent in web requests, including methods such as PUT, POST, or any other HTTP methods that support a request body. + It also supports deserialization of the body data, allowing for the interpretation of serialized content. .PARAMETER Deserialize - Specifies that the parameter value should be deserialized. When this switch is used, the value will be interpreted + Specifies that the body data should be deserialized. When this switch is used, the body data will be interpreted based on the provided style and other deserialization options. -.PARAMETER Explode - Specifies whether to explode arrays when deserializing the parameter value. This is useful when parameters contain - comma-separated values. Applicable only when the `-Deserialize` switch is used. +.PARAMETER NoExplode + Prevents deserialization from exploding arrays in the body data. This is useful when handling parameters that + contain comma-separated values and when array expansion is not desired. Applicable only when the `-Deserialize` + switch is used. .PARAMETER Style - Defines the deserialization style to use when interpreting the parameter value. Valid options are 'Simple', 'Label', - and 'Matrix'. The default is 'Simple'. Applicable only when the `-Deserialize` switch is used. + Defines the deserialization style to use when interpreting the body data. Valid options are 'Simple', 'Label', + 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. The default is 'Form'. Applicable only + when the `-Deserialize` switch is used. .PARAMETER ParameterName - Specifies the key name to use when deserializing the parameter value. The default value is 'id'. - This option is useful for mapping the parameter data accurately during deserialization. Applicable only - when the `-Deserialize` switch is used. + Specifies the key name to use when deserializing the body data. The default value is 'id'. This option is useful + for mapping the body data accurately during deserialization. Applicable only when the `-Deserialize` switch is used. .EXAMPLE - Get-PodePathParameter -Name 'action' - Returns the value of the 'action' parameter from the current web event. + Get-PodeBodyData + Returns the body data of the current web event. .EXAMPLE - Get-PodePathParameter -Name 'item' -Deserialize -Style 'Label' -Explode - Retrieves and deserializes the value of the 'item' parameter using the 'Label' style and exploding arrays. + Get-PodeBodyData -Deserialize -Style 'Matrix' + Retrieves and deserializes the body data using the 'Matrix' style. .EXAMPLE - Get-PodePathParameter -Name 'id' -Deserialize -KeyName 'userId' - Deserializes the 'id' parameter using the key name 'userId'. + Get-PodeBodyData -Deserialize -NoExplode + Deserializes the body data without exploding arrays. .NOTES - This function should be used within a route's script block in a Pode server. - The `-Deserialize` switch enables more advanced handling of complex data structures. + This function should be used within a route's script block in a Pode server. The `-Deserialize` switch enables + advanced handling of complex body data structures. #> -function Get-PodePathParameter { +function Get-PodeBodyData { [CmdletBinding(DefaultParameterSetName = 'BuiltIn' )] param( - [Parameter(Mandatory, ParameterSetName = 'Deserialize')] - [Parameter(Mandatory, ParameterSetName = 'BuiltIn')] - [string] - $Name, - [Parameter(Mandatory = $true, ParameterSetName = 'Deserialize')] [switch] $Deserialize, [Parameter(ParameterSetName = 'Deserialize')] [switch] - $Explode, + $NoExplode, [Parameter(ParameterSetName = 'Deserialize')] - [ValidateSet('Simple', 'Label', 'Matrix')] + [ValidateSet('Simple', 'Label', 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject')] [string] - $Style = 'Simple', + $Style = 'Form', [Parameter(ParameterSetName = 'Deserialize')] [string] $ParameterName = 'id' - ) if ($WebEvent) { if ($Deserialize.IsPresent) { - return ConvertFrom-PodeSerializedString -SerializedInput $WebEvent.Parameters[$Name] -Style $Style -Explode:$Explode -ParameterName $ParameterName + return ConvertFrom-PodeSerializedString -SerializedInput $WebEvent.Data -Style $Style -Explode:(!$NoExplode) -ParameterName $ParameterName } - return $WebEvent.Parameters[$Name] + return $WebEvent.Data } } + <# .SYNOPSIS Retrieves a specific query parameter value from the current Pode web event. @@ -4057,73 +2809,3 @@ function Get-PodeQueryParameter { } } -<# -.SYNOPSIS - Retrieves the body data from the current Pode web event. - -.DESCRIPTION - The `Get-PodeBodyData` function extracts and returns the body data of the current Pode web event. - This function is designed to access the main content sent in web requests, including methods such as PUT, POST, or any other HTTP methods that support a request body. - It also supports deserialization of the body data, allowing for the interpretation of serialized content. - -.PARAMETER Deserialize - Specifies that the body data should be deserialized. When this switch is used, the body data will be interpreted - based on the provided style and other deserialization options. - -.PARAMETER NoExplode - Prevents deserialization from exploding arrays in the body data. This is useful when handling parameters that - contain comma-separated values and when array expansion is not desired. Applicable only when the `-Deserialize` - switch is used. - -.PARAMETER Style - Defines the deserialization style to use when interpreting the body data. Valid options are 'Simple', 'Label', - 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. The default is 'Form'. Applicable only - when the `-Deserialize` switch is used. - -.PARAMETER ParameterName - Specifies the key name to use when deserializing the body data. The default value is 'id'. This option is useful - for mapping the body data accurately during deserialization. Applicable only when the `-Deserialize` switch is used. - -.EXAMPLE - Get-PodeBodyData - Returns the body data of the current web event. - -.EXAMPLE - Get-PodeBodyData -Deserialize -Style 'Matrix' - Retrieves and deserializes the body data using the 'Matrix' style. - -.EXAMPLE - Get-PodeBodyData -Deserialize -NoExplode - Deserializes the body data without exploding arrays. - -.NOTES - This function should be used within a route's script block in a Pode server. The `-Deserialize` switch enables - advanced handling of complex body data structures. -#> -function Get-PodeBodyData { - [CmdletBinding(DefaultParameterSetName = 'BuiltIn' )] - param( - [Parameter(Mandatory = $true, ParameterSetName = 'Deserialize')] - [switch] - $Deserialize, - - [Parameter(ParameterSetName = 'Deserialize')] - [switch] - $NoExplode, - - [Parameter(ParameterSetName = 'Deserialize')] - [ValidateSet('Simple', 'Label', 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject')] - [string] - $Style = 'Form', - - [Parameter(ParameterSetName = 'Deserialize')] - [string] - $ParameterName = 'id' - ) - if ($WebEvent) { - if ($Deserialize.IsPresent) { - return ConvertFrom-PodeSerializedString -SerializedInput $WebEvent.Data -Style $Style -Explode:(!$NoExplode) -ParameterName $ParameterName - } - return $WebEvent.Data - } -} \ No newline at end of file From 1d14ea2d0487918cd2ebbd4aa9a774b94cfa3388 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 22 Nov 2024 06:24:05 -0800 Subject: [PATCH 8/8] 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 } }