From 47f3cc85c13f734d9b0625f8db44d754a6e4a64b Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 8 Feb 2025 01:31:43 +0100 Subject: [PATCH 01/11] Remove Test-PSModuleTest function and add ConvertFrom-UriQueryString and ConvertTo-UriQueryString filters --- .../public/ConvertFrom-UriQueryString.ps1 | 90 +++++++ .../public/ConvertTo-UriQueryString.ps1 | 79 ++++++ src/functions/public/New-Uri.ps1 | 254 ++++++++++++++++++ src/functions/public/Test-PSModuleTest.ps1 | 18 -- 4 files changed, 423 insertions(+), 18 deletions(-) create mode 100644 src/functions/public/ConvertFrom-UriQueryString.ps1 create mode 100644 src/functions/public/ConvertTo-UriQueryString.ps1 create mode 100644 src/functions/public/New-Uri.ps1 delete mode 100644 src/functions/public/Test-PSModuleTest.ps1 diff --git a/src/functions/public/ConvertFrom-UriQueryString.ps1 b/src/functions/public/ConvertFrom-UriQueryString.ps1 new file mode 100644 index 0000000..87f9e3c --- /dev/null +++ b/src/functions/public/ConvertFrom-UriQueryString.ps1 @@ -0,0 +1,90 @@ +filter ConvertFrom-UriQueryString { + <# + .SYNOPSIS + Parses a URL query string into a hashtable of parameters. + + .DESCRIPTION + Takes a URI query string (the portion after the '?') and converts it into a hashtable + where each key is a parameter name and the corresponding value is the parameter value. + If the query string contains the same parameter multiple times, the resulting value + will be an array of those values. Percent-encoded characters in the input are decoded + back to their normal representation. + + .EXAMPLE + ConvertFrom-UriQueryString -QueryString 'name=John%20Doe&age=30&age=40' + + Output: + ```powershell + Name Value + ---- ----- + name John Doe + age {30, 40} + ``` + + Parses the given query string and returns a hashtable where keys are parameter names and + values are decoded parameter values. + + .EXAMPLE + ConvertFrom-UriQueryString '?q=PowerShell%20URI' + + Output: + ```powershell + Name Value + ---- ----- + q PowerShell URI + ``` + + Parses a query string that contains a single parameter and returns the corresponding value. + + .LINK + https://psmodule.io/Uri/Functions/ConvertFrom-UriQueryString/ + #> + [OutputType([hashtable])] + [CmdletBinding()] + param( + # The query string to parse. This can include the leading '?' or just the key-value pairs. + # For example, both "?foo=bar&count=10" and "foo=bar&count=10" are acceptable. + [Parameter(Mandatory, Position = 0, ValueFromPipeline)] + [string] $Query + ) + + Write-Verbose "Parsing query string: $Query" + # Remove leading '?' if present + $query = $Query + if ($query.StartsWith('?')) { + $query = $query.Substring(1) + } + if ([string]::IsNullOrEmpty($query)) { + return @{} # return empty hashtable if no query present + } + + $result = @{} + # Split by '&' to get each key=value pair + $pairs = $query.Split('&') + foreach ($pair in $pairs) { + if ([string]::IsNullOrWhiteSpace($pair)) { continue } # skip empty segments (e.g. "&&") + + $key, $val = $pair.Split('=', 2) # split into two parts at first '=' + $key = [System.Uri]::UnescapeDataString($key) + if ($val -ne $null) { + $val = [System.Uri]::UnescapeDataString($val) + } else { + $val = '' # if no '=' present, treat value as empty string + } + + if ($result.Contains($key)) { + # If key already exists, convert value to array or add to existing array + if ($result[$key] -is [System.Collections.IEnumerable] -and + $result[$key] -isnot [string]) { + # If already an array or collection, just add + $result[$key] += $val + } else { + # If a single value exists, turn it into an array + $result[$key] = @($result[$key], $val) + } + } else { + $result[$key] = $val + } + } + return $result +} diff --git a/src/functions/public/ConvertTo-UriQueryString.ps1 b/src/functions/public/ConvertTo-UriQueryString.ps1 new file mode 100644 index 0000000..a3956f0 --- /dev/null +++ b/src/functions/public/ConvertTo-UriQueryString.ps1 @@ -0,0 +1,79 @@ +filter ConvertTo-UriQueryString { + <# + .SYNOPSIS + Converts a hashtable of parameters into a URL query string. + + .DESCRIPTION + Takes a hashtable or dictionary of query parameters (keys and values) and constructs + a properly encoded query string (e.g. "key1=value1&key2=value2"). By default, all keys + and values are URL-encoded per RFC3986 rules to ensure the query string is valid. If a value + is an array, multiple entries for the same key are generated. Use -NoEncoding to skip encoding. + + .EXAMPLE + ConvertTo-UriQueryString -Query @{ foo = 'bar'; search = 'hello world'; ids = 1,2,3 } + + Output: + ```powershell + foo=bar&search=hello%20world&ids=1&ids=2&ids=3 + ``` + + Converts the hashtable into a URL-encoded query string. Spaces are replaced with `%20`. + + .EXAMPLE + ConvertTo-UriQueryString -Query @{ q = 'PowerShell'; verbose = $true } + + Output: + ```powershell + q=PowerShell&verbose=True + ``` + + Converts the query parameters into a valid query string. + + .LINK + https://psmodule.io/Uri/Functions/ConvertTo-UriQueryString + #> + [OutputType([string])] + [CmdletBinding()] + param( + # The hashtable (or IDictionary) containing parameter names and values. Each key becomes a parameter name. + # Values can be strings or other types convertible to string. If a value is an array or collection, each element + # in it will result in a separate instance of that parameter name in the output string. + [Parameter(Mandatory, Position = 0, ValueFromPipeline)] + [Alias('Params', 'Hashtable')] + [System.Collections.IDictionary] $Query, + + # If set, keys and values are not URL-encoded. Use this only if the inputs are already encoded or consist solely + # of characters safe in URLs. Without this, encoding is applied to escape special characters (e.g. spaces, &, =, #). + [Parameter()] + [switch] $NoEncoding + ) + + Write-Verbose "Converting hashtable to query string" + Write-Verbose "NoEncoding: $NoEncoding" + Write-Verbose "Query: $($Query | Out-String)" + + # Build the query string by iterating through each key-value pair + $pairs = @() + foreach ($key in $Query.Keys) { + $name = if ($NoEncoding) { $key.ToString() } else { [System.Uri]::EscapeDataString($key.ToString()) } + $value = $Query[$key] + + if ($null -eq $value) { + # Null value -> include key with empty value + $pairs += "$name=" + } elseif ([System.Collections.IEnumerable].IsAssignableFrom($value.GetType()) -and + -not ($value -is [string])) { + # If the value is a collection (and not a string, since strings are IEnumerable of chars), handle each. + foreach ($item in $value) { + $itemValue = if ($NoEncoding) { "$item" } else { [System.Uri]::EscapeDataString( ("$item") ) } + $pairs += "$name=$itemValue" + } + } else { + # Single value (includes strings, numbers, booleans, etc.) + $itemValue = if ($NoEncoding) { "$value" } else { [System.Uri]::EscapeDataString( ("$value") ) } + $pairs += "$name=$itemValue" + } + } + # Join all pairs with '&' and return + return [string]::Join('&', $pairs) +} diff --git a/src/functions/public/New-Uri.ps1 b/src/functions/public/New-Uri.ps1 new file mode 100644 index 0000000..e303ab2 --- /dev/null +++ b/src/functions/public/New-Uri.ps1 @@ -0,0 +1,254 @@ +function New-Uri { + <# + .SYNOPSIS + Constructs a URI from base, paths, query parameters, and fragment. + + .DESCRIPTION + Builds a URI string or object by combining a base URI with additional path segments, + query parameters, and an optional fragment. Ensures proper encoding (per RFC3986) + and correct placement of '/' in paths, handles query parameter merging, and appends + fragment identifiers. By default, returns a System.Uri object. + + .EXAMPLE + # Simple usage with base and path + New-Uri -BaseUri 'https://example.com' -Path 'products/item' + + Output: + ```powershell + https://example.com/products/item + ``` + + Constructs a URI with the given base and path. + + .EXAMPLE + # Adding query parameters via hashtable + New-Uri 'https://example.com/api' -Path 'search' -Query @{ q = 'test search'; page = @(2, 4) } + + Output: + ```powershell + https://example.com/api/search?q=test%20search&page=2 + ``` + + Adds query parameters to the URI, automatically encoding values. + + .EXAMPLE + # Merging with existing query and using -MergeQueryParameter + New-Uri 'https://example.com/data?year=2023' -Query @{ year = 2024; sort = 'asc' } -MergeQueryParameters + + Output: + ```powershell + https://example.com/data?year=2023&year=2024&sort=asc + ``` + + Merges new query parameters with the existing ones instead of replacing them. + + .OUTPUTS + System.Uri + + .OUTPUTS + System.UriBuilder + + .OUTPUTS + string + + .NOTES + - This function ensures URL encoding unless `-NoEncoding` is used. + - Merging query parameters allows keeping multiple values for the same key. + + .LINK + https://psmodule.io/Uri/Functions/New-Uri + #> + [OutputType(ParameterSetName = 'AsString', [string])] + [OutputType(ParameterSetName = 'AsUri', [System.Uri])] + [OutputType(ParameterSetName = 'AsUriBuilder', [System.UriBuilder])] + [CmdletBinding(DefaultParameterSetName = 'AsString')] + param( + # The base URI (string or [System.Uri]) to start from. + [Parameter(Mandatory, Position = 0)] + [Alias('Uri')] + [object] $BaseUri, + + # One or more path segments to append to the base URI. + [Parameter(Position = 1)] + [Alias('Paths')] + [string[]] $Path, + + # Query parameters to add to the URI. + [Parameter()] + [Alias('QueryParameters', 'QueryString')] + [object] $Query, + + # A URI fragment to append (the part after '#'). + [Parameter()] + [string] $Fragment, + + # If set, allows duplicate query keys instead of overriding. + [Parameter()] + [switch] $MergeQueryParameters, + + # Disables automatic URL encoding of path, query, and fragment. + [Parameter()] + [switch] $NoEncoding, + + # Outputs the resulting URI as a System.Uri object. + [Parameter(Mandatory, ParameterSetName = 'AsUri')] + [switch] $AsUri, + + # Outputs the resulting URI as a System.UriBuilder object. + [Parameter(Mandatory, ParameterSetName = 'AsUriBuilder')] + [switch] $AsUriBuilder + ) + + # Validate and prepare base URI + try { + # Accept [System.Uri] or string for base + $baseUriObj = if ($BaseUri -is [System.Uri]) { + $BaseUri + } else { + [System.Uri]::new([string]$BaseUri) # may throw if invalid + } + } catch { + throw "BaseUri '$BaseUri' is not a valid URI: $($_.Exception.Message)" + } + + # Use UriBuilder for convenient manipulation + $builder = [System.UriBuilder]::new($baseUriObj) + + # Handle path segments + if ($Path) { + $basePath = $builder.Path # e.g. "/" from 'https://example.com' + $segments = @() + + # If a single element containing '/' was passed, split it into segments. + if ($Path.Count -eq 1 -and $Path[0] -match '/') { + $segments = $Path[0].Split('/') | Where-Object { $_ -ne '' } + } else { + $segments = $Path + } + + # Normalize base path: ensure it ends with '/' if we need to append, except if base path is empty or just "/" + if ([string]::IsNullOrEmpty($basePath) -or $basePath -eq '/') { + $basePath = '' + } elseif ($basePath[-1] -ne '/') { + $basePath += '/' + } + + # Build combined path string from segments + $encodedSegments = @() + foreach ($seg in $segments) { + if ($NoEncoding) { + $encodedSegments += $seg + } else { + # Encode each segment individually (slashes are added between segments) + $encodedSegments += [System.Uri]::EscapeDataString($seg) + } + } + + $combinedPath = if ($basePath -ne '' -and $basePath -ne '/') { + "$basePath$([string]::Join('/', $encodedSegments))" + } else { + '/' + [string]::Join('/', $encodedSegments) + } + + # Preserve trailing slash if original single string ended with '/' + if ($Path.Count -eq 1 -and $Path[0].EndsWith('/')) { + $combinedPath += '/' + } + $builder.Path = $combinedPath + } + + + # Handle query parameters + if ($null -ne $Query) { + # Convert base URI's existing query to hashtable for merging (if any) + $baseQueryParams = @{} + if ($builder.Query -and $builder.Query.Length -gt 1) { + # builder.Query returns string starting with '?' + $existingQueryString = $builder.Query.Substring(1) # drop the '?' + $baseQueryParams = ConvertFrom-UriQueryString -Query $existingQueryString + } + + # Determine new query parameters from $Query input + $newQueryParams = @{} + if ($Query -is [hashtable] -or $Query -is [System.Collections.IDictionary]) { + $newQueryParams = $Query + } elseif ($Query -is [string]) { + # Remove leading '?' if present + $queryStr = $Query + if ($queryStr.StartsWith('?')) { $queryStr = $queryStr.Substring(1) } + if ($queryStr -ne '') { + $newQueryParams = ConvertFrom-UriQueryString -Query $queryStr + } + } else { + throw 'Query parameter must be a hashtable or query string (string).' + } + + # Merge base and new query params + $mergedParams = @{} + foreach ($key in $baseQueryParams.Keys) { + $mergedParams[$key] = $baseQueryParams[$key] + } + foreach ($key in $newQueryParams.Keys) { + if ($MergeQueryParameters -and $mergedParams.Contains($key)) { + # Merge same parameter: ensure value becomes an array of all values + $existingVal = $mergedParams[$key] + # Convert single existing value to array if not already + if ($null -ne $existingVal -and $existingVal.GetType().IsArray -eq $false) { + $existingVal = , $existingVal # wrap in array + } + $newVal = $newQueryParams[$key] + if ($null -ne $newVal -and $newVal.GetType().IsArray -eq $false) { + $newVal = , $newVal + } + # Combine arrays (or values) into one array + $combinedVal = @() + if ($existingVal) { $combinedVal += $existingVal } + if ($newVal) { $combinedVal += $newVal } + $mergedParams[$key] = $combinedVal + } else { + # If not merging duplicates, new value overwrites existing (or just adds if new) + $mergedParams[$key] = $newQueryParams[$key] + } + } + + # Convert merged hashtable to query string + $finalQueryString = ConvertTo-UriQueryString -Query $mergedParams -NoEncoding:$NoEncoding + $builder.Query = $finalQueryString # UriBuilder will prepend '?' automatically as needed + } else { + # If no new Query provided, but base URI had a query, ensure it's correctly encoded if NoEncoding is false. + # (UriBuilder.Query should have the base query already, and it is already encoded by System.Uri on BaseUriObj creation) + if ($NoEncoding) { + # If NoEncoding, we take the base query as-is (UriBuilder would have percent-encoded it already if base was string). + # Optionally, user might expect to keep percent-encoding from base even if NoEncoding is set. + # We'll leave it untouched. + } + } + + # Handle fragment + if ($PSBoundParameters.ContainsKey('Fragment')) { + # If Fragment is explicitly provided (even if empty string) + if ([string]::IsNullOrEmpty($Fragment)) { + # Empty fragment means remove any existing fragment + $builder.Fragment = '' # setting to empty string effectively removes fragment + } else { + $builder.Fragment = $NoEncoding ? ($Fragment -replace '^#', '') + : [System.Uri]::EscapeDataString(($Fragment -replace '^#', '')) + } + } + # (If fragment not provided, any fragment in base URI stays as is in builder.Fragment) + + # Output based on switches + switch ($PSCmdlet.ParameterSetName) { + 'AsUriBuilder' { + return $builder + } + 'AsUri' { + return $builder.Uri + } + default { + $uriString = "$($builder.Scheme)://$($builder.Host)$($builder.Uri.PathAndQuery)" + if ($builder.Fragment) { $uriString += "$($builder.Fragment)" -replace '(%20| )', '-' } + return $uriString + } + } +} diff --git a/src/functions/public/Test-PSModuleTest.ps1 b/src/functions/public/Test-PSModuleTest.ps1 deleted file mode 100644 index 26be2b9..0000000 --- a/src/functions/public/Test-PSModuleTest.ps1 +++ /dev/null @@ -1,18 +0,0 @@ -function Test-PSModuleTest { - <# - .SYNOPSIS - Performs tests on a module. - - .EXAMPLE - Test-PSModule -Name 'World' - - "Hello, World!" - #> - [CmdletBinding()] - param ( - # Name of the person to greet. - [Parameter(Mandatory)] - [string] $Name - ) - Write-Output "Hello, $Name!" -} From c1db7919a4c6807954ef7d5c4a5ab9e065e68b0a Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 8 Feb 2025 01:33:16 +0100 Subject: [PATCH 02/11] Add SkipTests option to Process-PSModule workflow --- .github/workflows/Process-PSModule.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/Process-PSModule.yml b/.github/workflows/Process-PSModule.yml index da48a8b..f95c5c0 100644 --- a/.github/workflows/Process-PSModule.yml +++ b/.github/workflows/Process-PSModule.yml @@ -28,3 +28,5 @@ jobs: Process-PSModule: uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v3 secrets: inherit + with: + SkipTests: All From 1e4ef0f802d604b7cab6f5197163db9711f85a57 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 8 Feb 2025 01:47:47 +0100 Subject: [PATCH 03/11] Refactor URI conversion functions for improved readability and consistency --- src/functions/public/ConvertFrom-UriQueryString.ps1 | 2 +- src/functions/public/ConvertTo-UriQueryString.ps1 | 4 ++-- src/functions/public/New-Uri.ps1 | 9 +++++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/functions/public/ConvertFrom-UriQueryString.ps1 b/src/functions/public/ConvertFrom-UriQueryString.ps1 index 87f9e3c..5b183fd 100644 --- a/src/functions/public/ConvertFrom-UriQueryString.ps1 +++ b/src/functions/public/ConvertFrom-UriQueryString.ps1 @@ -66,7 +66,7 @@ $key, $val = $pair.Split('=', 2) # split into two parts at first '=' $key = [System.Uri]::UnescapeDataString($key) - if ($val -ne $null) { + if ($null -ne $val) { $val = [System.Uri]::UnescapeDataString($val) } else { $val = '' # if no '=' present, treat value as empty string diff --git a/src/functions/public/ConvertTo-UriQueryString.ps1 b/src/functions/public/ConvertTo-UriQueryString.ps1 index a3956f0..e7f35d2 100644 --- a/src/functions/public/ConvertTo-UriQueryString.ps1 +++ b/src/functions/public/ConvertTo-UriQueryString.ps1 @@ -48,10 +48,10 @@ [switch] $NoEncoding ) - Write-Verbose "Converting hashtable to query string" + Write-Verbose 'Converting hashtable to query string' Write-Verbose "NoEncoding: $NoEncoding" Write-Verbose "Query: $($Query | Out-String)" - + # Build the query string by iterating through each key-value pair $pairs = @() foreach ($key in $Query.Keys) { diff --git a/src/functions/public/New-Uri.ps1 b/src/functions/public/New-Uri.ps1 index e303ab2..b9e4e76 100644 --- a/src/functions/public/New-Uri.ps1 +++ b/src/functions/public/New-Uri.ps1 @@ -58,10 +58,15 @@ .LINK https://psmodule.io/Uri/Functions/New-Uri #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', + Scope = 'Function', + Justification = 'Creates a new URI object without changing state' + )] [OutputType(ParameterSetName = 'AsString', [string])] [OutputType(ParameterSetName = 'AsUri', [System.Uri])] [OutputType(ParameterSetName = 'AsUriBuilder', [System.UriBuilder])] - [CmdletBinding(DefaultParameterSetName = 'AsString')] + [CmdletBinding(DefaultParameterSetName = 'AsUri')] param( # The base URI (string or [System.Uri]) to start from. [Parameter(Mandatory, Position = 0)] @@ -245,7 +250,7 @@ 'AsUri' { return $builder.Uri } - default { + 'AsString' { $uriString = "$($builder.Scheme)://$($builder.Host)$($builder.Uri.PathAndQuery)" if ($builder.Fragment) { $uriString += "$($builder.Fragment)" -replace '(%20| )', '-' } return $uriString From 2157440afd8e869ae531e6ae1d74f15fa1ea1bda Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 8 Feb 2025 01:53:41 +0100 Subject: [PATCH 04/11] Update New-Uri.ps1 to change output parameter from AsUri to AsString for clarity --- src/functions/public/New-Uri.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/functions/public/New-Uri.ps1 b/src/functions/public/New-Uri.ps1 index b9e4e76..9efa9d7 100644 --- a/src/functions/public/New-Uri.ps1 +++ b/src/functions/public/New-Uri.ps1 @@ -95,9 +95,9 @@ [Parameter()] [switch] $NoEncoding, - # Outputs the resulting URI as a System.Uri object. - [Parameter(Mandatory, ParameterSetName = 'AsUri')] - [switch] $AsUri, + # Outputs the resulting URI as a string. + [Parameter(Mandatory, ParameterSetName = 'AsString')] + [switch] $AsString, # Outputs the resulting URI as a System.UriBuilder object. [Parameter(Mandatory, ParameterSetName = 'AsUriBuilder')] From 0437a876f185d30cc7fbc2d0e20bb9ea0f02e553 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 8 Feb 2025 02:28:40 +0100 Subject: [PATCH 05/11] Remove aliases from parameters in New-Uri and ConvertTo-UriQueryString for clarity and consistency --- .../public/ConvertTo-UriQueryString.ps1 | 1 - src/functions/public/New-Uri.ps1 | 45 +++++++++++++++---- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/functions/public/ConvertTo-UriQueryString.ps1 b/src/functions/public/ConvertTo-UriQueryString.ps1 index e7f35d2..1533bdf 100644 --- a/src/functions/public/ConvertTo-UriQueryString.ps1 +++ b/src/functions/public/ConvertTo-UriQueryString.ps1 @@ -39,7 +39,6 @@ # Values can be strings or other types convertible to string. If a value is an array or collection, each element # in it will result in a separate instance of that parameter name in the output string. [Parameter(Mandatory, Position = 0, ValueFromPipeline)] - [Alias('Params', 'Hashtable')] [System.Collections.IDictionary] $Query, # If set, keys and values are not URL-encoded. Use this only if the inputs are already encoded or consist solely diff --git a/src/functions/public/New-Uri.ps1 b/src/functions/public/New-Uri.ps1 index 9efa9d7..dec1187 100644 --- a/src/functions/public/New-Uri.ps1 +++ b/src/functions/public/New-Uri.ps1 @@ -5,9 +5,9 @@ .DESCRIPTION Builds a URI string or object by combining a base URI with additional path segments, - query parameters, and an optional fragment. Ensures proper encoding (per RFC3986) + query parameters, and an optional fragment. Ensures proper encoding (per [RFC3986](https://datatracker.ietf.org/doc/html/rfc3986)) and correct placement of '/' in paths, handles query parameter merging, and appends - fragment identifiers. By default, returns a System.Uri object. + fragment identifiers. By default, returns a `[System.Uri]` object. .EXAMPLE # Simple usage with base and path @@ -15,29 +15,58 @@ Output: ```powershell - https://example.com/products/item + AbsolutePath : /products/item + AbsoluteUri : https://example.com/products/item + LocalPath : /products/item + Authority : example.com + HostNameType : Dns + IsDefaultPort : True + IsFile : False + IsLoopback : False + PathAndQuery : /products/item + Segments : {/, products/, item} + IsUnc : False + Host : example.com + Port : 443 + Query : + Fragment : + Scheme : https + OriginalString : https://example.com:443/products/item + DnsSafeHost : example.com + IdnHost : example.com + IsAbsoluteUri : True + UserEscaped : False + UserInfo : ``` Constructs a URI with the given base and path. .EXAMPLE # Adding query parameters via hashtable - New-Uri 'https://example.com/api' -Path 'search' -Query @{ q = 'test search'; page = @(2, 4) } + New-Uri 'https://example.com/api' -Path 'search' -Query @{ q = 'test search'; page = @(2, 4) } -AsUriBuilder Output: ```powershell - https://example.com/api/search?q=test%20search&page=2 + Scheme : https + UserName : + Password : + Host : example.com + Port : 443 + Path : /api/search + Query : ?q=test%20search&page=2&page=4 + Fragment : + Uri : https://example.com/api/search?q=test search&page=2&page=4 ``` Adds query parameters to the URI, automatically encoding values. .EXAMPLE # Merging with existing query and using -MergeQueryParameter - New-Uri 'https://example.com/data?year=2023' -Query @{ year = 2024; sort = 'asc' } -MergeQueryParameters + New-Uri 'https://example.com/data?year=2023' -Query @{ year = 2024; sort = 'asc' } -MergeQueryParameters -AsString Output: ```powershell - https://example.com/data?year=2023&year=2024&sort=asc + https://example.com/data?sort=asc&year=2023&year=2024 ``` Merges new query parameters with the existing ones instead of replacing them. @@ -75,12 +104,10 @@ # One or more path segments to append to the base URI. [Parameter(Position = 1)] - [Alias('Paths')] [string[]] $Path, # Query parameters to add to the URI. [Parameter()] - [Alias('QueryParameters', 'QueryString')] [object] $Query, # A URI fragment to append (the part after '#'). From 903d3d142c3265a424decf956c57d5c059a554ed Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 8 Feb 2025 02:32:51 +0100 Subject: [PATCH 06/11] Refactor URI tests and remove deprecated Test-PSModuleTest function --- .github/workflows/Process-PSModule.yml | 2 - tests/Uri.Tests.ps1 | 135 ++++++++++++++++++++++++- 2 files changed, 132 insertions(+), 5 deletions(-) diff --git a/.github/workflows/Process-PSModule.yml b/.github/workflows/Process-PSModule.yml index f95c5c0..da48a8b 100644 --- a/.github/workflows/Process-PSModule.yml +++ b/.github/workflows/Process-PSModule.yml @@ -28,5 +28,3 @@ jobs: Process-PSModule: uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v3 secrets: inherit - with: - SkipTests: All diff --git a/tests/Uri.Tests.ps1 b/tests/Uri.Tests.ps1 index 671fa98..1b065af 100644 --- a/tests/Uri.Tests.ps1 +++ b/tests/Uri.Tests.ps1 @@ -1,5 +1,134 @@ -Describe 'Module' { - It 'Function: Test-PSModuleTest' { - Test-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' +Describe 'Uri' { + + Context 'Function: ConvertFrom-UriQueryString' { + + It 'ConvertFrom-UriQueryString - returns empty hashtable for empty input' { + $result = ConvertFrom-UriQueryString -Query '' + $result | Should -BeOfType 'Hashtable' + $result.Count | Should -Be 0 + + $result2 = ConvertFrom-UriQueryString -Query '?' + $result2.Count | Should -Be 0 + } + + It 'ConvertFrom-UriQueryString - decodes percent-encoded characters' { + $result = ConvertFrom-UriQueryString -Query '?q=PowerShell%20URI' + $result.q | Should -Be 'PowerShell URI' + } + + It 'ConvertFrom-UriQueryString - handles multiple values for same key' { + $result = ConvertFrom-UriQueryString -Query 'name=John%20Doe&age=30&age=40' + $result.name | Should -Be 'John Doe' + # When the key repeats, the value becomes an array + $result.age | Should -BeOfType 'Object[]' + $result.age[0] | Should -Be '30' + $result.age[1] | Should -Be '40' + } + + It 'ConvertFrom-UriQueryString - removes leading question mark if present' { + $result1 = ConvertFrom-UriQueryString -Query '?foo=bar' + $result2 = ConvertFrom-UriQueryString -Query 'foo=bar' + $result1.foo | Should -Be 'bar' + $result2.foo | Should -Be 'bar' + } + + It 'ConvertFrom-UriQueryString - treats key with no value as empty string' { + $result = ConvertFrom-UriQueryString -Query 'foo' + $result.foo | Should -Be '' + } + } + + Context 'Function: ConvertTo-UriQueryString' { + + It 'ConvertTo-UriQueryString - builds query string from hashtable with single values' { + $query = @{ foo = 'bar'; search = 'hello world' } + $result = ConvertTo-UriQueryString -Query $query + # Order is not guaranteed so check for expected pairs: + $pairs = $result -split '&' + $pairs | Should -Contain 'foo=bar' + $pairs | Should -Contain 'search=hello%20world' + } + + It 'ConvertTo-UriQueryString - handles array values producing multiple parameters' { + $query = @{ ids = 1, 2, 3 } + $result = ConvertTo-UriQueryString -Query $query + $pairs = $result -split '&' + $pairs | Should -Contain 'ids=1' + $pairs | Should -Contain 'ids=2' + $pairs | Should -Contain 'ids=3' + } + + It 'ConvertTo-UriQueryString - outputs query string without encoding when NoEncoding is used' { + $query = @{ foo = 'hello world'; bar = 'a=b c' } + $result = ConvertTo-UriQueryString -Query $query -NoEncoding + $pairs = $result -split '&' + $pairs | Should -Contain 'foo=hello world' + $pairs | Should -Contain 'bar=a=b c' + } + + It 'ConvertTo-UriQueryString - handles null values producing key with empty value' { + $query = @{ foo = $null } + $result = ConvertTo-UriQueryString -Query $query + $result | Should -Be 'foo=' + } + } + + Context 'Function: New-Uri' { + + It 'New-Uri - constructs a URI with base and appended path' { + $uri = New-Uri -BaseUri 'https://example.com' -Path 'products/item' + # Expect a System.Uri object with the proper path + $uri.AbsoluteUri | Should -Be 'https://example.com/products/item' + } + + It 'New-Uri - adds query parameters from hashtable correctly' { + $uri = New-Uri -BaseUri 'https://example.com/api' -Path 'search' -Query @{ q = 'test search'; page = 2 } + $query = $uri.Query.TrimStart('?') + $pairs = $query -split '&' + $pairs | Should -Contain 'q=test%20search' + $pairs | Should -Contain 'page=2' + } + + It 'New-Uri - merges query parameters when MergeQueryParameters switch is used' { + $uri = New-Uri -BaseUri 'https://example.com/data?year=2023' -Query @{ year = 2024; sort = 'asc' } -MergeQueryParameters + $query = $uri.Query.TrimStart('?') + $pairs = $query -split '&' + $pairs | Should -Contain 'year=2023' + $pairs | Should -Contain 'year=2024' + $pairs | Should -Contain 'sort=asc' + } + + It 'New-Uri - appends fragment to URI' { + $uri = New-Uri -BaseUri 'https://example.com/path' -Fragment 'section1' + # The resulting URI should have the fragment appended after '#' + $uri.AbsoluteUri | Should -Match '#section1$' + } + + It 'New-Uri - returns string output when AsString switch is used' { + $uriString = New-Uri -BaseUri 'https://example.com' -Path 'test' -AsString + $uriString | Should -BeOfType 'string' + $uriString | Should -Match '^https://example\.com/test' + } + + It 'New-Uri - returns System.Uri object by default' { + $uri = New-Uri -BaseUri 'https://example.com' + $uri | Should -BeOfType 'System.Uri' + } + + It 'New-Uri - throws error for invalid BaseUri' { + { New-Uri -BaseUri 'notaurl' } | Should -Throw + } + + It 'New-Uri - accepts query string as Query parameter input' { + $uri = New-Uri -BaseUri 'https://example.com/api' -Path 'search' -Query '?q=hello%20world' + $query = $uri.Query.TrimStart('?') + $query | Should -Match 'q=hello%20world' + } + + It 'New-Uri - respects NoEncoding switch for path segments' { + $uri = New-Uri -BaseUri 'https://example.com' -Path 'a b/c d' -NoEncoding -AsString + # With NoEncoding, spaces should remain as-is in the path + $uri | Should -Match 'https://example\.com/a b/c d' + } } } From 0bdd3e0f273917b7b9f75ec6232f7809e2b96b5c Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 8 Feb 2025 02:41:10 +0100 Subject: [PATCH 07/11] Enhance ConvertFrom-UriQueryString to handle null or empty query strings gracefully --- .../public/ConvertFrom-UriQueryString.ps1 | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/functions/public/ConvertFrom-UriQueryString.ps1 b/src/functions/public/ConvertFrom-UriQueryString.ps1 index 5b183fd..3ab8fdc 100644 --- a/src/functions/public/ConvertFrom-UriQueryString.ps1 +++ b/src/functions/public/ConvertFrom-UriQueryString.ps1 @@ -44,23 +44,29 @@ param( # The query string to parse. This can include the leading '?' or just the key-value pairs. # For example, both "?foo=bar&count=10" and "foo=bar&count=10" are acceptable. - [Parameter(Mandatory, Position = 0, ValueFromPipeline)] + [Parameter(Position = 0, ValueFromPipeline)] + [AllowNull()] [string] $Query ) + # Early exit if $Query is null or empty. + if ([string]::IsNullOrEmpty($Query)) { + Write-Verbose 'Query string is null or empty.' + return @{} + } + Write-Verbose "Parsing query string: $Query" # Remove leading '?' if present - $query = $Query - if ($query.StartsWith('?')) { - $query = $query.Substring(1) + if ($Query.StartsWith('?')) { + $Query = $Query.Substring(1) } - if ([string]::IsNullOrEmpty($query)) { + if ([string]::IsNullOrEmpty($Query)) { return @{} # return empty hashtable if no query present } $result = @{} # Split by '&' to get each key=value pair - $pairs = $query.Split('&') + $pairs = $Query.Split('&') foreach ($pair in $pairs) { if ([string]::IsNullOrWhiteSpace($pair)) { continue } # skip empty segments (e.g. "&&") From 4fc8f15a88f206467df77055522c373395846aa6 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 8 Feb 2025 02:49:37 +0100 Subject: [PATCH 08/11] Remove NoEncoding parameter and ensure query strings are always encoded in New-Uri function --- src/functions/public/New-Uri.ps1 | 40 +++++++------------------------- tests/Uri.Tests.ps1 | 10 +------- 2 files changed, 10 insertions(+), 40 deletions(-) diff --git a/src/functions/public/New-Uri.ps1 b/src/functions/public/New-Uri.ps1 index dec1187..ec24695 100644 --- a/src/functions/public/New-Uri.ps1 +++ b/src/functions/public/New-Uri.ps1 @@ -118,10 +118,6 @@ [Parameter()] [switch] $MergeQueryParameters, - # Disables automatic URL encoding of path, query, and fragment. - [Parameter()] - [switch] $NoEncoding, - # Outputs the resulting URI as a string. [Parameter(Mandatory, ParameterSetName = 'AsString')] [switch] $AsString, @@ -133,7 +129,6 @@ # Validate and prepare base URI try { - # Accept [System.Uri] or string for base $baseUriObj = if ($BaseUri -is [System.Uri]) { $BaseUri } else { @@ -165,15 +160,10 @@ $basePath += '/' } - # Build combined path string from segments + # Build combined path string from segments, always encoding $encodedSegments = @() foreach ($seg in $segments) { - if ($NoEncoding) { - $encodedSegments += $seg - } else { - # Encode each segment individually (slashes are added between segments) - $encodedSegments += [System.Uri]::EscapeDataString($seg) - } + $encodedSegments += [System.Uri]::EscapeDataString($seg) } $combinedPath = if ($basePath -ne '' -and $basePath -ne '/') { @@ -189,7 +179,6 @@ $builder.Path = $combinedPath } - # Handle query parameters if ($null -ne $Query) { # Convert base URI's existing query to hashtable for merging (if any) @@ -238,36 +227,25 @@ if ($newVal) { $combinedVal += $newVal } $mergedParams[$key] = $combinedVal } else { - # If not merging duplicates, new value overwrites existing (or just adds if new) + # New value overwrites or adds $mergedParams[$key] = $newQueryParams[$key] } } - # Convert merged hashtable to query string - $finalQueryString = ConvertTo-UriQueryString -Query $mergedParams -NoEncoding:$NoEncoding - $builder.Query = $finalQueryString # UriBuilder will prepend '?' automatically as needed - } else { - # If no new Query provided, but base URI had a query, ensure it's correctly encoded if NoEncoding is false. - # (UriBuilder.Query should have the base query already, and it is already encoded by System.Uri on BaseUriObj creation) - if ($NoEncoding) { - # If NoEncoding, we take the base query as-is (UriBuilder would have percent-encoded it already if base was string). - # Optionally, user might expect to keep percent-encoding from base even if NoEncoding is set. - # We'll leave it untouched. - } + # Convert merged hashtable to query string (always encoding) + $finalQueryString = ConvertTo-UriQueryString -Query $mergedParams + $builder.Query = $finalQueryString # UriBuilder handles the '?' automatically } # Handle fragment if ($PSBoundParameters.ContainsKey('Fragment')) { - # If Fragment is explicitly provided (even if empty string) if ([string]::IsNullOrEmpty($Fragment)) { - # Empty fragment means remove any existing fragment - $builder.Fragment = '' # setting to empty string effectively removes fragment + $builder.Fragment = '' # remove any existing fragment } else { - $builder.Fragment = $NoEncoding ? ($Fragment -replace '^#', '') - : [System.Uri]::EscapeDataString(($Fragment -replace '^#', '')) + $builder.Fragment = [System.Uri]::EscapeDataString(($Fragment -replace '^#', '')) } } - # (If fragment not provided, any fragment in base URI stays as is in builder.Fragment) + # (If fragment not provided, any fragment in base URI stays as is) # Output based on switches switch ($PSCmdlet.ParameterSetName) { diff --git a/tests/Uri.Tests.ps1 b/tests/Uri.Tests.ps1 index 1b065af..8e26495 100644 --- a/tests/Uri.Tests.ps1 +++ b/tests/Uri.Tests.ps1 @@ -20,7 +20,7 @@ $result = ConvertFrom-UriQueryString -Query 'name=John%20Doe&age=30&age=40' $result.name | Should -Be 'John Doe' # When the key repeats, the value becomes an array - $result.age | Should -BeOfType 'Object[]' + $result.age | Should -HaveCount 2 $result.age[0] | Should -Be '30' $result.age[1] | Should -Be '40' } @@ -58,14 +58,6 @@ $pairs | Should -Contain 'ids=3' } - It 'ConvertTo-UriQueryString - outputs query string without encoding when NoEncoding is used' { - $query = @{ foo = 'hello world'; bar = 'a=b c' } - $result = ConvertTo-UriQueryString -Query $query -NoEncoding - $pairs = $result -split '&' - $pairs | Should -Contain 'foo=hello world' - $pairs | Should -Contain 'bar=a=b c' - } - It 'ConvertTo-UriQueryString - handles null values producing key with empty value' { $query = @{ foo = $null } $result = ConvertTo-UriQueryString -Query $query From b20a5f308eaf5a00e328c69b7d4faa52d27c9b81 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 8 Feb 2025 02:55:37 +0100 Subject: [PATCH 09/11] Remove tests for NoEncoding switch in New-Uri to reflect recent parameter removal --- tests/Uri.Tests.ps1 | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/Uri.Tests.ps1 b/tests/Uri.Tests.ps1 index 8e26495..1753328 100644 --- a/tests/Uri.Tests.ps1 +++ b/tests/Uri.Tests.ps1 @@ -116,11 +116,5 @@ $query = $uri.Query.TrimStart('?') $query | Should -Match 'q=hello%20world' } - - It 'New-Uri - respects NoEncoding switch for path segments' { - $uri = New-Uri -BaseUri 'https://example.com' -Path 'a b/c d' -NoEncoding -AsString - # With NoEncoding, spaces should remain as-is in the path - $uri | Should -Match 'https://example\.com/a b/c d' - } } } From dee661dd01a9b9837d613b16a48cae8ff80eb8de Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 8 Feb 2025 04:49:09 +0100 Subject: [PATCH 10/11] Enhance URI module by enabling debug and verbose logging in workflow, removing NoEncoding references, and updating query string handling for clarity. Also, add advanced usage examples to documentation. --- .github/workflows/Process-PSModule.yml | 3 + README.md | 104 +++++++++---- examples/General.md | 145 ++++++++++++++++++ examples/General.ps1 | 19 --- .../public/ConvertFrom-UriQueryString.ps1 | 17 +- .../public/ConvertTo-UriQueryString.ps1 | 25 +-- src/functions/public/New-Uri.ps1 | 1 - 7 files changed, 237 insertions(+), 77 deletions(-) create mode 100644 examples/General.md delete mode 100644 examples/General.ps1 diff --git a/.github/workflows/Process-PSModule.yml b/.github/workflows/Process-PSModule.yml index da48a8b..3126fd7 100644 --- a/.github/workflows/Process-PSModule.yml +++ b/.github/workflows/Process-PSModule.yml @@ -28,3 +28,6 @@ jobs: Process-PSModule: uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v3 secrets: inherit + with: + Debug: true + Verbose: true diff --git a/README.md b/README.md index 96936ee..af5f246 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,111 @@ -# {{ NAME }} +# URI -{{ DESCRIPTION }} +URI is a PowerShell module that provides robust functions for parsing, constructing, and manipulating URIs. It offers easy-to-use commands to: + +- Parse URL query strings into hashtables. +- Convert hashtables (or dictionaries) into properly URL-encoded query strings. +- Build complete URIs from base addresses, path segments, query parameters, and fragments—with automatic handling of URL encoding per [RFC3986](https://datatracker.ietf.org/doc/html/rfc3986). ## Prerequisites -This uses the following external resources: -- The [PSModule framework](https://github.com/PSModule) for building, testing and publishing the module. +This module uses the following external resources: +- The [PSModule framework](https://github.com/PSModule) for building, testing, and publishing the module. ## Installation -To install the module from the PowerShell Gallery, you can use the following command: +To install the module from the PowerShell Gallery, run the following commands: ```powershell -Install-PSResource -Name {{ NAME }} -Import-Module -Name {{ NAME }} +Install-PSResource -Name Uri +Import-Module -Name Uri ``` ## Usage -Here is a list of example that are typical use cases for the module. +The **URI** module includes several functions to work with URIs. Below are common use cases and examples: -### Example 1: Greet an entity +### 1. Parsing a Query String -Provide examples for typical commands that a user would like to do with the module. +Use `ConvertFrom-UriQueryString` to convert a URL query string into a hashtable of parameters. This function decodes percent-encoded characters and handles duplicate keys by returning an array of values. ```powershell -Greet-Entity -Name 'World' -Hello, World! +# Example: Parsing a query string with multiple values for the same key. +$parsed = ConvertFrom-UriQueryString -Query 'name=John%20Doe&age=30&age=40' +$parsed ``` -### Example 2 +Expected Output: + +```powershell +Name Value +---- ----- +name John Doe +age {30, 40} +``` + +### 2. Constructing a Query String + +Use `ConvertTo-UriQueryString` to convert a hashtable (or dictionary) of parameters into a URL-encoded query string. If a value is an array, multiple key-value pairs will be generated. + +```powershell +# Example: Converting a hashtable of parameters into a query string. +$queryString = ConvertTo-UriQueryString -Query @{ foo = 'bar'; search = 'hello world'; ids = 1,2,3 } +$queryString +``` -Provide examples for typical commands that a user would like to do with the module. +Expected Output: ```powershell -Import-Module -Name PSModuleTemplate +foo=bar&search=hello%20world&ids=1&ids=2&ids=3 ``` -### Find more examples +### 3. Building a Complete URI -To find more examples of how to use the module, please refer to the [examples](examples) folder. +Use `New-Uri` to construct a URI by combining a base URI with optional path segments, query parameters, and a fragment. -Alternatively, you can use the Get-Command -Module 'This module' to find more commands that are available in the module. -To find examples of each of the commands you can use Get-Help -Examples 'CommandName'. +```powershell +# Example 1: Building a URI with a base and a path. +$uri = New-Uri -BaseUri 'https://example.com' -Path 'products/item' +$uri +``` + +Expected Output (as a `[System.Uri]` object): + +```powershell +AbsoluteUri : https://example.com/products/item +... +``` + +```powershell +# Example 2: Constructing a URI while merging existing query parameters. +$uriString = New-Uri 'https://example.com/data?year=2023' -Query @{ year = 2024; sort = 'asc' } -MergeQueryParameters -AsString +$uriString +``` + +Expected Output: + +```powershell +https://example.com/data?sort=asc&year=2023&year=2024 +``` ## Documentation -Link to further documentation if available, or describe where in the repository users can find more detailed documentation about -the module's functions and features. +For more detailed documentation about each function and additional examples, please refer to the [documentation](docs) folder in the repository or view the detailed help in PowerShell: + +```powershell +Get-Help ConvertFrom-UriQueryString -Detailed +Get-Help ConvertTo-UriQueryString -Detailed +Get-Help New-Uri -Detailed +``` ## Contributing -Coder or not, you can contribute to the project! We welcome all contributions. +Contributions are welcome—whether you're a user or a developer! ### For Users -If you don't code, you still sit on valuable information that can make this project even better. If you experience that the -product does unexpected things, throw errors or is missing functionality, you can help by submitting bugs and feature requests. -Please see the issues tab on this project and submit a new issue that matches your needs. +If you encounter issues, unexpected behavior, or have feature requests, please submit a new issue via the repository's Issues tab. ### For Developers -If you do code, we'd love to have your contributions. Please read the [Contribution guidelines](CONTRIBUTING.md) for more information. -You can either help by picking up an existing issue or submit a new one if you have an idea for a new feature or improvement. - -## Acknowledgements - -Here is a list of people and projects that helped this project in some way. +We appreciate your help in making this module even better. Please review our [Contribution Guidelines](CONTRIBUTING.md) before submitting pull requests. You can start by picking up an existing issue or proposing new features or improvements. diff --git a/examples/General.md b/examples/General.md new file mode 100644 index 0000000..6b833e7 --- /dev/null +++ b/examples/General.md @@ -0,0 +1,145 @@ +# Advanced Examples for the URI Module + +This document demonstrates advanced usage scenarios for the **URI** module. These examples cover more complex cases, including handling duplicate query parameters, merging queries, and constructing URIs with multiple path segments and fragments. + +--- + +## Example 1: Parsing a Complex Query String + +When a query string includes duplicate keys and URL-encoded characters, the `ConvertFrom-UriQueryString` function returns arrays for keys with multiple values. + +```powershell +# A query string with duplicate keys and an empty value. +$query = 'filter=active&filter=recent&search=PowerShell%20URI&empty' +$result = ConvertFrom-UriQueryString -Query $query +$result +``` + +**Expected Output:** + +```none +Name Value +---- ----- +empty +search PowerShell URI +filter {active, recent} +``` + +--- + +## Example 2: Converting a Hashtable with Array Values to a Query String + +The `ConvertTo-UriQueryString` function handles hashtables where some keys have array values by generating multiple key-value pairs. + +```powershell +# A hashtable where 'tags' contains multiple values. +$params = @{ + category = 'books' + tags = @('fiction', 'bestseller', '2023') + available = $true +} +$queryString = ConvertTo-UriQueryString -Query $params +$queryString +``` + +**Expected Output:** + +```none +category=books&tags=fiction&tags=bestseller&tags=2023&available=True +``` + +--- + +## Example 3: Merging Existing and New Query Parameters + +Use `New-Uri` with the `-MergeQueryParameters` switch to merge new query parameters into a base URI that already includes a query string. + +```powershell +# Base URI with an existing 'page' parameter. +$baseUri = 'https://example.com/api/items?page=1' +$newParams = @{ page = @(2,3); sort = 'desc' } +$uri = New-Uri -BaseUri $baseUri -Query $newParams -MergeQueryParameters -AsUri +$uri +``` + +**Expected Output:** + +The resulting URI should merge the original and new query parameters, producing duplicate `page` keys: + +```none +Scheme : https +UserName : +Password : +Host : example.com +Port : 443 +Path : /api/items +Query : ?page=1&page=2&page=3&sort=desc +Fragment : +Uri : https://example.com/api/items?page=1&page=2&page=3&sort=desc +``` + +--- + +## Example 4: Constructing a URI with Multiple Path Segments and a Fragment + +`New-Uri` accepts an array of path segments and a fragment, constructing a well-formed URI. + +```powershell +# Combining multiple path segments and appending a fragment. +$uri = New-Uri -BaseUri 'https://example.com' -Path @('catalog', 'books', 'fiction') -Fragment 'section2' +$uri +``` + +**Expected Output:** + +A URI similar to: + +```none +AbsolutePath : /catalog/books/fiction +AbsoluteUri : https://example.com/catalog/books/fiction#section2 +LocalPath : /catalog/books/fiction +Authority : example.com +HostNameType : Dns +IsDefaultPort : True +IsFile : False +IsLoopback : False +PathAndQuery : /catalog/books/fiction +Segments : {/, catalog/, books/, fiction} +IsUnc : False +Host : example.com +Port : 443 +Query : +Fragment : #section2 +Scheme : https +OriginalString : https://example.com:443/catalog/books/fiction#section2 +DnsSafeHost : example.com +IdnHost : example.com +IsAbsoluteUri : True +UserEscaped : False +UserInfo : +``` + +--- + +## Example 5: Producing a Custom-Formatted URI String + +If you prefer the final URI as a string, use the `-AsString` switch. In this advanced example, notice how the fragment is appended after custom formatting. + +```powershell +# Create a URI string with a custom formatted fragment. +$uriString = New-Uri -BaseUri 'https://example.com/store' -Path 'items/special offers/' -Fragment 'limited edition' -AsString +$uriString +``` + +**Expected Output:** + +The output will be a formatted URI string with the fragment adjusted (e.g., spaces replaced with hyphens): + +```none +https://example.com/store/items/special%20offers/#limited-edition +``` + +--- + +These advanced examples illustrate how to leverage the full power of the **URI** module for complex scenarios. +For additional details or further use cases, consult the module’s documentation or source code. diff --git a/examples/General.ps1 b/examples/General.ps1 deleted file mode 100644 index e193423..0000000 --- a/examples/General.ps1 +++ /dev/null @@ -1,19 +0,0 @@ -<# - .SYNOPSIS - This is a general example of how to use the module. -#> - -# Import the module -Import-Module -Name 'PSModule' - -# Define the path to the font file -$FontFilePath = 'C:\Fonts\CodeNewRoman\CodeNewRomanNerdFontPropo-Regular.tff' - -# Install the font -Install-Font -Path $FontFilePath -Verbose - -# List installed fonts -Get-Font -Name 'CodeNewRomanNerdFontPropo-Regular' - -# Uninstall the font -Get-Font -Name 'CodeNewRomanNerdFontPropo-Regular' | Uninstall-Font -Verbose diff --git a/src/functions/public/ConvertFrom-UriQueryString.ps1 b/src/functions/public/ConvertFrom-UriQueryString.ps1 index 3ab8fdc..2ad05ca 100644 --- a/src/functions/public/ConvertFrom-UriQueryString.ps1 +++ b/src/functions/public/ConvertFrom-UriQueryString.ps1 @@ -70,26 +70,25 @@ foreach ($pair in $pairs) { if ([string]::IsNullOrWhiteSpace($pair)) { continue } # skip empty segments (e.g. "&&") - $key, $val = $pair.Split('=', 2) # split into two parts at first '=' + $key, $value = $pair.Split('=', 2) # split into two parts at first '=' $key = [System.Uri]::UnescapeDataString($key) - if ($null -ne $val) { - $val = [System.Uri]::UnescapeDataString($val) + if ($null -ne $value) { + $value = [System.Uri]::UnescapeDataString($value) } else { - $val = '' # if no '=' present, treat value as empty string + $value = '' # if no '=' present, treat value as empty string } if ($result.Contains($key)) { # If key already exists, convert value to array or add to existing array - if ($result[$key] -is [System.Collections.IEnumerable] -and - $result[$key] -isnot [string]) { + if ($result[$key] -is [System.Collections.IEnumerable] -and $result[$key] -isnot [string]) { # If already an array or collection, just add - $result[$key] += $val + $result[$key] += $value } else { # If a single value exists, turn it into an array - $result[$key] = @($result[$key], $val) + $result[$key] = @($result[$key], $value) } } else { - $result[$key] = $val + $result[$key] = $value } } return $result diff --git a/src/functions/public/ConvertTo-UriQueryString.ps1 b/src/functions/public/ConvertTo-UriQueryString.ps1 index 1533bdf..c956332 100644 --- a/src/functions/public/ConvertTo-UriQueryString.ps1 +++ b/src/functions/public/ConvertTo-UriQueryString.ps1 @@ -7,7 +7,7 @@ Takes a hashtable or dictionary of query parameters (keys and values) and constructs a properly encoded query string (e.g. "key1=value1&key2=value2"). By default, all keys and values are URL-encoded per RFC3986 rules to ensure the query string is valid. If a value - is an array, multiple entries for the same key are generated. Use -NoEncoding to skip encoding. + is an array, multiple entries for the same key are generated. .EXAMPLE ConvertTo-UriQueryString -Query @{ foo = 'bar'; search = 'hello world'; ids = 1,2,3 } @@ -39,40 +39,31 @@ # Values can be strings or other types convertible to string. If a value is an array or collection, each element # in it will result in a separate instance of that parameter name in the output string. [Parameter(Mandatory, Position = 0, ValueFromPipeline)] - [System.Collections.IDictionary] $Query, - - # If set, keys and values are not URL-encoded. Use this only if the inputs are already encoded or consist solely - # of characters safe in URLs. Without this, encoding is applied to escape special characters (e.g. spaces, &, =, #). - [Parameter()] - [switch] $NoEncoding + [System.Collections.IDictionary] $Query ) - Write-Verbose 'Converting hashtable to query string' - Write-Verbose "NoEncoding: $NoEncoding" + Write-Verbose 'Converting hashtable to query string with URL encoding' Write-Verbose "Query: $($Query | Out-String)" - # Build the query string by iterating through each key-value pair $pairs = @() foreach ($key in $Query.Keys) { - $name = if ($NoEncoding) { $key.ToString() } else { [System.Uri]::EscapeDataString($key.ToString()) } + # URL-encode the key. + $name = [System.Uri]::EscapeDataString($key.ToString()) $value = $Query[$key] if ($null -eq $value) { # Null value -> include key with empty value $pairs += "$name=" - } elseif ([System.Collections.IEnumerable].IsAssignableFrom($value.GetType()) -and - -not ($value -is [string])) { - # If the value is a collection (and not a string, since strings are IEnumerable of chars), handle each. + } elseif ([System.Collections.IEnumerable].IsAssignableFrom($value.GetType()) -and -not ($value -is [string])) { foreach ($item in $value) { - $itemValue = if ($NoEncoding) { "$item" } else { [System.Uri]::EscapeDataString( ("$item") ) } + $itemValue = [System.Uri]::EscapeDataString("$item") $pairs += "$name=$itemValue" } } else { # Single value (includes strings, numbers, booleans, etc.) - $itemValue = if ($NoEncoding) { "$value" } else { [System.Uri]::EscapeDataString( ("$value") ) } + $itemValue = [System.Uri]::EscapeDataString("$value") $pairs += "$name=$itemValue" } } - # Join all pairs with '&' and return return [string]::Join('&', $pairs) } diff --git a/src/functions/public/New-Uri.ps1 b/src/functions/public/New-Uri.ps1 index ec24695..47b2cbb 100644 --- a/src/functions/public/New-Uri.ps1 +++ b/src/functions/public/New-Uri.ps1 @@ -81,7 +81,6 @@ string .NOTES - - This function ensures URL encoding unless `-NoEncoding` is used. - Merging query parameters allows keeping multiple values for the same key. .LINK From daf96423a985221d76b140f113d9027c8fb51600 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 8 Feb 2025 05:18:06 +0100 Subject: [PATCH 11/11] Remove debug and verbose logging options from Process-PSModule workflow --- .github/workflows/Process-PSModule.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/Process-PSModule.yml b/.github/workflows/Process-PSModule.yml index 3126fd7..da48a8b 100644 --- a/.github/workflows/Process-PSModule.yml +++ b/.github/workflows/Process-PSModule.yml @@ -28,6 +28,3 @@ jobs: Process-PSModule: uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v3 secrets: inherit - with: - Debug: true - Verbose: true