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..b620d3a54 --- /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 + +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: + +- **`-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..f1222aa2e --- /dev/null +++ b/docs/Tutorials/Routes/Parameters/Cookies.md @@ -0,0 +1,107 @@ + +# 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. + + + +#### 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: + +```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 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 new file mode 100644 index 000000000..913b860bf --- /dev/null +++ b/docs/Tutorials/Routes/Parameters/Headers.md @@ -0,0 +1,102 @@ +# 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. + +#### 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: + +```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 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 new file mode 100644 index 000000000..e1a3d3e5a --- /dev/null +++ b/docs/Tutorials/Routes/Parameters/Paths.md @@ -0,0 +1,108 @@ + +# 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'`. + +#### 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: + +```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. + +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 new file mode 100644 index 000000000..41b839b98 --- /dev/null +++ b/docs/Tutorials/Routes/Parameters/Queries.md @@ -0,0 +1,107 @@ +# 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'`. + +#### 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: + +```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. + +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/Pode.psd1 b/src/Pode.psd1 index aff2bfcb7..fe186f204 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -141,6 +141,11 @@ 'ConvertFrom-PodeXml', 'Set-PodeDefaultFolder', 'Get-PodeDefaultFolder', + 'Get-PodeBodyData', + 'Get-PodeQueryParameter', + 'Get-PodePathParameter', + 'ConvertFrom-PodeSerializedString', + 'ConvertTo-PodeSerializedString', 'Get-PodeCurrentRunspaceName', 'Set-PodeCurrentRunspaceName', 'Invoke-PodeGC', 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 e193bbbc3..25310b73e 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -1520,13 +1520,13 @@ function ConvertFrom-PodeXml { <# .SYNOPSIS -Invokes the garbage collector. + Invokes the garbage collector. .DESCRIPTION -Invokes the garbage collector. + Invokes the garbage collector. .EXAMPLE -Invoke-PodeGC + Invoke-PodeGC #> function Invoke-PodeGC { [CmdletBinding()] @@ -1611,3 +1611,1326 @@ function Start-PodeSleep { + +<# +.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. + +.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 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. + +.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] + $ParameterName = 'id' + + ) + if ($WebEvent) { + if ($Deserialize.IsPresent) { + return ConvertFrom-PodeSerializedString -SerializedInput $WebEvent.Parameters[$Name] -Style $Style -Explode:$Explode -ParameterName $ParameterName + } + return $WebEvent.Parameters[$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 + 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] + } +} + diff --git a/tests/unit/Utility.Tests.ps1 b/tests/unit/Utility.Tests.ps1 new file mode 100644 index 000000000..2c9f5d0db --- /dev/null +++ b/tests/unit/Utility.Tests.ps1 @@ -0,0 +1,956 @@ +[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 -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 -SerializedInput $serialized -Style Simple -Explode + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + # 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 -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 -SerializedInput $serialized -Style Simple + $result | Should -be '5' + } + + It 'Convert Simple style serialized string to hashtable' { + $serialized = 'role,admin,firstName,Alex' + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + # 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 -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 -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 -SerializedInput $serialized -Style Label -Explode + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + # 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 -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 -SerializedInput $serialized -Style Label + $result | Should -be 5 + } + + It 'Convert Label style serialized string to hashtable' { + $serialized = '.role,admin,firstName,Alex' + $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Label + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + # 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 -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 -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 -SerializedInput $serialized -Style Matrix -Explode + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + # 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 -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 -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 -SerializedInput $serialized -Style Matrix + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + # 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 -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 -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 -SerializedInput $serialized -Style Matrix -Explode + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + # 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 -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 -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 -SerializedInput $serialized -Style Matrix + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + # 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 -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 -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 -SerializedInput $serialized -Style Form -Explode + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + # 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 -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 -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 -SerializedInput $serialized -Style Form + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + # 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 -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 -SerializedInput $serialized -Style SpaceDelimited -Explode + $result | Should -be @('3', '4', '5') + } + + It 'Convert SpaceDelimited style serialized string to array' { + $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 -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 -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 -SerializedInput $serialized -Style DeepObject + $expected = @{ + role = 'admin' + firstName = 'Alex' + } + # 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 -SerializedInput $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 -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 -SerializedInput $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 -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 -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 -SerializedInput $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 -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 -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 -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 -SerializedInput $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 -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] + } + } + + } + +} + + + +Describe 'ConvertTo-PodeSerializedString' { + + BeforeAll { + function SortSerializedString { + param ( + [string] $SerializedString, + [string] $Delimiter, + [switch] $GroupPairs, + [string] $SkipHead = '', + [string] $RemovePattern = '' + ) + + # 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 -InputObject $hashtable -Style 'Simple' + $sortedResult = SortSerializedString -SerializedInput $result -Delimiter ',' -GroupPairs + $expected = 'name,value,number,10,anotherName,anotherValue' + $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter ',' -GroupPairs + $sortedResult | Should -Be $sortedExpected + } + + It 'should convert hashtable to Simple style serialized string with Explode' { + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + number = 10 + } + $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Simple' -Explode + $sortedResult = SortSerializedString -SerializedInput $result -Delimiter ',' + $expected = 'name=value,number=10,anotherName=anotherValue' + $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter ',' + $sortedResult | Should -Be $sortedExpected + } + + It 'should convert hashtable to Label style serialized string' { + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + number = 10 + } + $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Label' + + $sortedResult = SortSerializedString -SerializedInput $result -Delimiter ',' -SkipHead '.' + $expected = '.anotherName,anotherValue,number,10,name,value' + $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter ',' -SkipHead '.' + $sortedResult | Should -Be $sortedExpected + } + + It 'should convert hashtable to Label style serialized string with Explode' { + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + number = 10 + } + $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Label' -Explode + $sortedResult = SortSerializedString -SerializedInput $result -Delimiter ',' -SkipHead '.' + $expected = '.anotherName=anotherValue,number=10,name=value' + $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter ',' -SkipHead '.' + $sortedResult | Should -Be $sortedExpected + } + + It 'should convert hashtable to Matrix style serialized string' { + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + number = 10 + } + $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 -SerializedInput $expected -Delimiter ',' -GroupPairs -SkipHead ';id=' + $sortedResult | Should -Be $sortedExpected + } + + It 'should convert hashtable to Matrix style serialized string with Explode' { + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + number = 10 + } + $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Matrix' -Explode + $sortedResult = SortSerializedString -SerializedInput $result -Delimiter ';' -SkipHead ';' + $expected = ';name=value;number=10;anotherName=anotherValue' + $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter ';' -SkipHead ';' + $sortedResult | Should -Be $sortedExpected + } + + It 'should convert hashtable to Form style serialized string' { + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + number = 10 + } + $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 -SerializedInput $expected -Delimiter ',' -GroupPairs -SkipHead '?id=' + $sortedResult | Should -Be $sortedExpected + } + + It 'should convert hashtable to Form style serialized string with Explode' { + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + number = 10 + } + $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Form' -Explode + $sortedResult = SortSerializedString -SerializedInput $result -Delimiter '&' -SkipHead '?' + $expected = '?name=value&number=10&anotherName=anotherValue' + $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter '&' -SkipHead '?' + $sortedResult | Should -Be $sortedExpected + } + + + + It 'should convert hashtable to DeepObject style serialized string' { + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + number = 10 + } + $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 -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 | 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%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 | Should -Be '?id=3&id=4&id=5' + } + + + It 'should throw an error for unsupported serialization style' { + $hashtable = @{ + name = 'value' + anotherName = 'anotherValue' + number = 10 + } + { 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' + } + + +} + + +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