Public/Routes.ps1

<#
.SYNOPSIS
Adds a Route for a specific HTTP Method(s).
 
.DESCRIPTION
Adds a Route for a specific HTTP Method(s), with path, that when called with invoke any logic and/or Middleware.
 
.PARAMETER Method
The HTTP Method of this Route, multiple can be supplied.
 
.PARAMETER Path
The URI path for the Route.
 
.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware.
 
.PARAMETER ScriptBlock
A ScriptBlock for the Route's main logic.
 
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) this Route should be bound against.
 
.PARAMETER ContentType
The content type the Route should use when parsing any payloads.
 
.PARAMETER TransferEncoding
The transfer encoding the Route should use when parsing any payloads.
 
.PARAMETER ErrorContentType
The content type of any error pages that may get returned.
 
.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Route's main logic.
 
.PARAMETER ArgumentList
An array of arguments to supply to the Route's ScriptBlock.
 
.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on this Route.
 
.PARAMETER Access
The name of an Access method which should be used as middleware on this Route.
 
.PARAMETER AllowAnon
If supplied, the Route will allow anonymous access for non-authenticated users.
 
.PARAMETER Login
If supplied, the Route will be flagged to Authentication as being a Route that handles user logins.
 
.PARAMETER Logout
If supplied, the Route will be flagged to Authentication as being a Route that handles users logging out.
 
.PARAMETER PassThru
If supplied, the route created will be returned so it can be passed through a pipe.
 
.PARAMETER IfExists
Specifies what action to take when a Route already exists. (Default: Default)
 
.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER User
One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER OAResponses
An alternative way to associate OpenApi responses unsing New-PodeOAResponse instead of piping multiple Add-PodeOAResponse
 
.PARAMETER OAReference
A reference to OpenAPI reusable pathItem component created with Add-PodeOAComponentPathItem
 
.PARAMETER OADefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
 
.EXAMPLE
Add-PodeRoute -Method Get -Path '/' -ScriptBlock { /* logic */ }
 
.EXAMPLE
Add-PodeRoute -Method Post -Path '/users/:userId/message' -Middleware (Get-PodeCsrfMiddleware) -ScriptBlock { /* logic */ }
 
.EXAMPLE
Add-PodeRoute -Method Post -Path '/user' -ContentType 'application/json' -ScriptBlock { /* logic */ }
 
.EXAMPLE
Add-PodeRoute -Method Post -Path '/user' -ContentType 'application/json' -TransferEncoding gzip -ScriptBlock { /* logic */ }
 
.EXAMPLE
Add-PodeRoute -Method Get -Path '/api/cpu' -ErrorContentType 'application/json' -ScriptBlock { /* logic */ }
 
.EXAMPLE
Add-PodeRoute -Method Get -Path '/' -ScriptBlock { /* logic */ } -ArgumentList 'arg1', 'arg2'
 
.EXAMPLE
Add-PodeRoute -Method Get -Path '/' -Role 'Developer', 'QA' -ScriptBlock { /* logic */ }
 
.EXAMPLE
$Responses = New-PodeOAResponse -StatusCode 400 -Description 'Invalid username supplied' |
            New-PodeOAResponse -StatusCode 404 -Description 'User not found' |
            New-PodeOAResponse -StatusCode 405 -Description 'Invalid Input'
 
Add-PodeRoute -PassThru -Method Put -Path '/user/:username' -OAResponses $Responses -ScriptBlock {
            #code is going here
        }
#>

function Add-PodeRoute {
    [CmdletBinding(DefaultParameterSetName = 'Script')]
    [OutputType([System.Object[]])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
        [string[]]
        $Method,

        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [object[]]
        $Middleware,

        [Parameter(ParameterSetName = 'Script')]
        [scriptblock]
        $ScriptBlock,

        [Parameter( )]
        [AllowNull()]
        [string[]]
        $EndpointName,

        [Parameter()]
        [string]
        $ContentType,

        [Parameter()]
        [ValidateSet('', 'gzip', 'deflate')]
        [string]
        $TransferEncoding,

        [Parameter()]
        [string]
        $ErrorContentType,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $FilePath,

        [Parameter()]
        [object[]]
        $ArgumentList,

        [Parameter()]
        [Alias('Auth')]
        [string]
        $Authentication,

        [Parameter()]
        [string]
        $Access,

        [Parameter()]
        [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
        [string]
        $IfExists = 'Default',

        [Parameter()]
        [string[]]
        $Role,

        [Parameter()]
        [string[]]
        $Group,

        [Parameter()]
        [string[]]
        $Scope,

        [Parameter()]
        [string[]]
        $User,

        [switch]
        $AllowAnon,

        [switch]
        $Login,

        [switch]
        $Logout,

        [hashtable]
        $OAResponses,

        [string]
        $OAReference,

        [switch]
        $PassThru,

        [string[]]
        $OADefinitionTag
    )

    # check if we have any route group info defined
    if ($null -ne $RouteGroup) {
        if (![string]::IsNullOrWhiteSpace($RouteGroup.Path)) {
            $Path = "$($RouteGroup.Path)$($Path)"
        }

        if ($null -ne $RouteGroup.Middleware) {
            $Middleware = $RouteGroup.Middleware + $Middleware
        }

        if ([string]::IsNullOrWhiteSpace($EndpointName)) {
            $EndpointName = $RouteGroup.EndpointName
        }

        if ([string]::IsNullOrWhiteSpace($ContentType)) {
            $ContentType = $RouteGroup.ContentType
        }

        if ([string]::IsNullOrWhiteSpace($TransferEncoding)) {
            $TransferEncoding = $RouteGroup.TransferEncoding
        }

        if ([string]::IsNullOrWhiteSpace($ErrorContentType)) {
            $ErrorContentType = $RouteGroup.ErrorContentType
        }

        if ([string]::IsNullOrWhiteSpace($Authentication)) {
            $Authentication = $RouteGroup.Authentication
        }

        if ([string]::IsNullOrWhiteSpace($Access)) {
            $Access = $RouteGroup.Access
        }

        if ($RouteGroup.AllowAnon) {
            $AllowAnon = $RouteGroup.AllowAnon
        }

        if ($RouteGroup.IfExists -ine 'default') {
            $IfExists = $RouteGroup.IfExists
        }

        if ($null -ne $RouteGroup.AccessMeta.Role) {
            $Role = $RouteGroup.AccessMeta.Role + $Role
        }

        if ($null -ne $RouteGroup.AccessMeta.Group) {
            $Group = $RouteGroup.AccessMeta.Group + $Group
        }

        if ($null -ne $RouteGroup.AccessMeta.Scope) {
            $Scope = $RouteGroup.AccessMeta.Scope + $Scope
        }

        if ($null -ne $RouteGroup.AccessMeta.User) {
            $User = $RouteGroup.AccessMeta.User + $User
        }

        if ($null -ne $RouteGroup.AccessMeta.Custom) {
            $CustomAccess = $RouteGroup.AccessMeta.Custom
        }

        if ($null -ne $RouteGroup.OADefinitionTag ) {
            $OADefinitionTag = $RouteGroup.OADefinitionTag
        }
    }

    # var for new routes created
    $newRoutes = @()

    # store the original path
    $origPath = $Path

    # split route on '?' for query
    $Path = Split-PodeRouteQuery -Path $Path
    if ([string]::IsNullOrWhiteSpace($Path)) {
        # No Path supplied for the Route
        throw ($PodeLocale.noPathSuppliedForRouteExceptionMessage)
    }

    # ensure the route has appropriate slashes
    $Path = Update-PodeRouteSlash -Path $Path
    $OpenApiPath = ConvertTo-PodeOARoutePath -Path $Path
    $Path = Resolve-PodePlaceholder -Path $Path

    # get endpoints from name
    $endpoints = Find-PodeEndpoint -EndpointName $EndpointName

    # get default route IfExists state
    if ($IfExists -ieq 'Default') {
        $IfExists = Get-PodeRouteIfExistsPreference
    }

    # if middleware, scriptblock and file path are all null/empty, error
    if ((Test-PodeIsEmpty $Middleware) -and (Test-PodeIsEmpty $ScriptBlock) -and (Test-PodeIsEmpty $FilePath) -and (Test-PodeIsEmpty $Authentication)) {
        # [Method] Path: No logic passed
        throw ($PodeLocale.noLogicPassedForMethodRouteExceptionMessage -f ($Method -join ','), $Path)
    }

    # if we have a file path supplied, load that path as a scriptblock
    if ($PSCmdlet.ParameterSetName -ieq 'file') {
        $ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
    }

    # check for scoped vars
    $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

    # convert any middleware into valid hashtables
    $Middleware = @(ConvertTo-PodeMiddleware -Middleware $Middleware -PSSession $PSCmdlet.SessionState)

    # if an access name was supplied, setup access as middleware first to it's after auth middleware
    if (![string]::IsNullOrWhiteSpace($Access)) {
        if ([string]::IsNullOrWhiteSpace($Authentication)) {
            # Access requires Authentication to be supplied on Routes
            throw ($PodeLocale.accessRequiresAuthenticationOnRoutesExceptionMessage)
        }

        if (!(Test-PodeAccessExists -Name $Access)) {
            # Access method does not exist
            throw ($PodeLocale.accessMethodDoesNotExistExceptionMessage -f $Access)
        }

        $options = @{
            Name = $Access
        }

        $Middleware = (@(Get-PodeAccessMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware)
    }

    # if an auth name was supplied, setup the auth as the first middleware
    if (![string]::IsNullOrWhiteSpace($Authentication)) {
        if (!(Test-PodeAuthExists -Name $Authentication)) {
            # Authentication method does not exist
            throw ($PodeLocale.authenticationMethodDoesNotExistExceptionMessage -f $Authentication)
        }

        $options = @{
            Name   = $Authentication
            Login  = $Login
            Logout = $Logout
            Anon   = $AllowAnon
        }

        $Middleware = (@(Get-PodeAuthMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware)
    }

    # custom access
    if ($null -eq $CustomAccess) {
        $CustomAccess = @{}
    }

    # workout a default content type for the route
    $ContentType = Find-PodeRouteContentType -Path $Path -ContentType $ContentType

    # workout a default transfer encoding for the route
    $TransferEncoding = Find-PodeRouteTransferEncoding -Path $Path -TransferEncoding $TransferEncoding

    # loop through each method
    foreach ($_method in $Method) {
        # ensure the route doesn't already exist for each endpoint
        $endpoints = @(foreach ($_endpoint in $endpoints) {
                $found = Test-PodeRouteInternal -Method $_method -Path $Path -Protocol $_endpoint.Protocol -Address $_endpoint.Address -ThrowError:($IfExists -ieq 'Error')

                if ($found) {
                    if ($IfExists -ieq 'Overwrite') {
                        Remove-PodeRoute -Method $_method -Path $origPath -EndpointName $_endpoint.Name
                    }

                    if ($IfExists -ieq 'Skip') {
                        continue
                    }
                }

                $_endpoint
            })

        if (($null -eq $endpoints) -or ($endpoints.Length -eq 0)) {
            continue
        }

        #add security header method if autoMethods is enabled
        if (  $PodeContext.Server.Security.autoMethods ) {
            Add-PodeSecurityHeader -Name 'Access-Control-Allow-Methods' -Value $_method.ToUpper() -Append
        }

        $DefinitionTag = Test-PodeOADefinitionTag -Tag $OADefinitionTag

        #add the default OpenApi responses
        if ( $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.defaultResponses) {
            $DefaultResponse = [ordered]@{}
            foreach ($tag in $DefinitionTag) {
                $DefaultResponse[$tag] = Copy-PodeObjectDeepClone -InputObject $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.defaultResponses
            }
        }

        # add the route(s)
        Write-Verbose "Adding Route: [$($_method)] $($Path)"
        $methodRoutes = @(foreach ($_endpoint in $endpoints) {
                @{
                    Logic            = $ScriptBlock
                    UsingVariables   = $usingVars
                    Middleware       = $Middleware
                    Authentication   = $Authentication
                    Access           = $Access
                    AccessMeta       = @{
                        Role   = $Role
                        Group  = $Group
                        Scope  = $Scope
                        User   = $User
                        Custom = $CustomAccess
                    }
                    Endpoint         = @{
                        Protocol = $_endpoint.Protocol
                        Address  = $_endpoint.Address.Trim()
                        Name     = $_endpoint.Name
                    }
                    ContentType      = $ContentType
                    TransferEncoding = $TransferEncoding
                    ErrorType        = $ErrorContentType
                    Arguments        = $ArgumentList
                    Method           = $_method
                    Path             = $Path
                    OpenApi          = @{
                        Path               = $OpenApiPath
                        Responses          = $DefaultResponse
                        Parameters         = [ordered]@{}
                        RequestBody        = [ordered]@{}
                        CallBacks          = [ordered]@{}
                        Authentication     = @()
                        Servers            = @()
                        DefinitionTag      = $DefinitionTag
                        IsDefTagConfigured = ($null -ne $OADefinitionTag) #Definition Tag has been configured (Not default)
                    }
                    IsStatic         = $false
                    Metrics          = @{
                        Requests = @{
                            Total       = 0
                            StatusCodes = @{}
                        }
                    }
                }
            })


        if ($PodeContext.Server.OpenAPI.Routes -notcontains $OpenApiPath ) {
            $PodeContext.Server.OpenAPI.Routes += $OpenApiPath
        }


        if (![string]::IsNullOrWhiteSpace($Authentication)) {
            Set-PodeOAAuth -Route $methodRoutes -Name $Authentication -AllowAnon:$AllowAnon
        }

        $PodeContext.Server.Routes[$_method][$Path] += @($methodRoutes)
        if ($PassThru) {
            $newRoutes += $methodRoutes
        }
    }
    if ($OAReference) {
        Test-PodeOAComponentInternal -Field pathItems -DefinitionTag $DefinitionTag -Name $OAReference -PostValidation
        foreach ($r in @($newRoutes)) {
            $r.OpenApi = @{
                '$ref'        = "#/components/paths/$OAReference"
                DefinitionTag = $DefinitionTag
                Path          = $OpenApiPath
            }
        }
    }
    elseif ($OAResponses) {
        foreach ($r in @($newRoutes)) {
            $r.OpenApi.Responses = $OAResponses
        }
    }

    # return the routes?
    if ($PassThru) {
        return $newRoutes
    }
}

<#
.SYNOPSIS
Add a static Route for rendering static content.
 
.DESCRIPTION
Add a static Route for rendering static content. You can also define default pages to display.
 
.PARAMETER Path
The URI path for the static Route.
 
.PARAMETER Source
The literal, or relative, path to the directory that contains the static content.
 
.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware.
 
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) to bind the static Route against.
 
.PARAMETER ContentType
The content type the static Route should use when parsing any payloads.
 
.PARAMETER TransferEncoding
The transfer encoding the static Route should use when parsing any payloads.
 
.PARAMETER Defaults
An array of default pages to display, such as 'index.html'.
 
.PARAMETER ErrorContentType
The content type of any error pages that may get returned.
 
.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on this Route.
 
.PARAMETER Access
The name of an Access method which should be used as middleware on this Route.
 
.PARAMETER AllowAnon
If supplied, the static route will allow anonymous access for non-authenticated users.
 
.PARAMETER DownloadOnly
When supplied, all static content on this Route will be attached as downloads - rather than rendered.
 
.PARAMETER PassThru
If supplied, the static route created will be returned so it can be passed through a pipe.
 
.PARAMETER IfExists
Specifies what action to take when a Static Route already exists. (Default: Default)
 
.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER User
One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER FileBrowser
If supplied, when the path is a folder, instead of returning 404, will return A browsable content of the directory.
 
.PARAMETER RedirectToDefault
If supplied, the user will be redirected to the default page if found instead of the page being rendered as the folder path.
 
.EXAMPLE
Add-PodeStaticRoute -Path '/assets' -Source './assets'
 
.EXAMPLE
Add-PodeStaticRoute -Path '/assets' -Source './assets' -Defaults @('index.html')
 
.EXAMPLE
Add-PodeStaticRoute -Path '/installers' -Source './exes' -DownloadOnly
 
.EXAMPLE
Add-PodeStaticRoute -Path '/assets' -Source './assets' -Defaults @('index.html') -RedirectToDefault
#>

function Add-PodeStaticRoute {
    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter(Mandatory = $true)]
        [string]
        $Source,

        [Parameter()]
        [object[]]
        $Middleware,

        [Parameter()]
        [string[]]
        $EndpointName,

        [Parameter()]
        [string]
        $ContentType,

        [Parameter()]
        [ValidateSet('', 'gzip', 'deflate')]
        [string]
        $TransferEncoding,

        [Parameter()]
        [string[]]
        $Defaults,

        [Parameter()]
        [string]
        $ErrorContentType,

        [Parameter()]
        [Alias('Auth')]
        [string]
        $Authentication,

        [Parameter()]
        [string]
        $Access,

        [Parameter()]
        [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
        [string]
        $IfExists = 'Default',

        [Parameter()]
        [string[]]
        $Role,

        [Parameter()]
        [string[]]
        $Group,

        [Parameter()]
        [string[]]
        $Scope,

        [Parameter()]
        [string[]]
        $User,

        [switch]
        $AllowAnon,

        [switch]
        $DownloadOnly,

        [switch]
        $FileBrowser,

        [switch]
        $PassThru,

        [switch]
        $RedirectToDefault
    )

    # check if we have any route group info defined
    if ($null -ne $RouteGroup) {
        if (![string]::IsNullOrWhiteSpace($RouteGroup.Path)) {
            $Path = "$($RouteGroup.Path)$($Path)"
        }

        if (![string]::IsNullOrWhiteSpace($RouteGroup.Source)) {
            $Source = [System.IO.Path]::Combine($Source, $RouteGroup.Source.TrimStart('\/'))
        }

        if ($null -ne $RouteGroup.Middleware) {
            $Middleware = $RouteGroup.Middleware + $Middleware
        }

        if ([string]::IsNullOrWhiteSpace($EndpointName)) {
            $EndpointName = $RouteGroup.EndpointName
        }

        if ([string]::IsNullOrWhiteSpace($ContentType)) {
            $ContentType = $RouteGroup.ContentType
        }

        if ([string]::IsNullOrWhiteSpace($TransferEncoding)) {
            $TransferEncoding = $RouteGroup.TransferEncoding
        }

        if ([string]::IsNullOrWhiteSpace($ErrorContentType)) {
            $ErrorContentType = $RouteGroup.ErrorContentType
        }

        if ([string]::IsNullOrWhiteSpace($Authentication)) {
            $Authentication = $RouteGroup.Authentication
        }

        if ([string]::IsNullOrWhiteSpace($Access)) {
            $Access = $RouteGroup.Access
        }

        if (Test-PodeIsEmpty $Defaults) {
            $Defaults = $RouteGroup.Defaults
        }

        if ($RouteGroup.AllowAnon) {
            $AllowAnon = $RouteGroup.AllowAnon
        }

        if ($RouteGroup.DownloadOnly) {
            $DownloadOnly = $RouteGroup.DownloadOnly
        }

        if ($RouteGroup.FileBrowser) {
            $FileBrowser = $RouteGroup.FileBrowser
        }

        if ($RouteGroup.RedirectToDefault) {
            $RedirectToDefault = $RouteGroup.RedirectToDefault
        }

        if ($RouteGroup.IfExists -ine 'default') {
            $IfExists = $RouteGroup.IfExists
        }

        if ($null -ne $RouteGroup.AccessMeta.Role) {
            $Role = $RouteGroup.AccessMeta.Role + $Role
        }

        if ($null -ne $RouteGroup.AccessMeta.Group) {
            $Group = $RouteGroup.AccessMeta.Group + $Group
        }

        if ($null -ne $RouteGroup.AccessMeta.Scope) {
            $Scope = $RouteGroup.AccessMeta.Scope + $Scope
        }

        if ($null -ne $RouteGroup.AccessMeta.User) {
            $User = $RouteGroup.AccessMeta.User + $User
        }

        if ($null -ne $RouteGroup.AccessMeta.Custom) {
            $CustomAccess = $RouteGroup.AccessMeta.Custom
        }
    }

    # store the route method
    $Method = 'Static'

    # store the original path
    $origPath = $Path

    # split route on '?' for query
    $Path = Split-PodeRouteQuery -Path $Path
    if ([string]::IsNullOrWhiteSpace($Path)) {
        # No Path supplied for the Route.
        throw ($PodeLocale.noPathSuppliedForRouteExceptionMessage)
    }

    # ensure the route has appropriate slashes
    $Path = Update-PodeRouteSlash -Path $Path -Static
    $OpenApiPath = ConvertTo-PodeOARoutePath -Path $Path
    $Path = Resolve-PodePlaceholder -Path $Path

    # get endpoints from name
    $endpoints = Find-PodeEndpoint -EndpointName $EndpointName

    # get default route IfExists state
    if ($IfExists -ieq 'Default') {
        $IfExists = Get-PodeRouteIfExistsPreference
    }

    # ensure the route doesn't already exist for each endpoint
    $endpoints = @(foreach ($_endpoint in $endpoints) {
            $found = Test-PodeRouteInternal -Method $Method -Path $Path -Protocol $_endpoint.Protocol -Address $_endpoint.Address -ThrowError:($IfExists -ieq 'Error')

            if ($found) {
                if ($IfExists -ieq 'Overwrite') {
                    Remove-PodeStaticRoute -Path $origPath -EndpointName $_endpoint.Name
                }

                if ($IfExists -ieq 'Skip') {
                    continue
                }
            }

            $_endpoint
        })

    if (($null -eq $endpoints) -or ($endpoints.Length -eq 0)) {
        return
    }

    # if static, ensure the path exists at server root
    $Source = Get-PodeRelativePath -Path $Source -JoinRoot
    if (!(Test-PodePath -Path $Source -NoStatus)) {
        # [Method)] Path: The Source path supplied for Static Route does not exist
        throw ($PodeLocale.sourcePathDoesNotExistForStaticRouteExceptionMessage -f $Path, $Source)
    }

    # setup a temp drive for the path
    $Source = New-PodePSDrive -Path $Source

    # setup default static files
    if ($null -eq $Defaults) {
        $Defaults = Get-PodeStaticRouteDefault
    }

    if (!$RedirectToDefault) {
        $RedirectToDefault = $PodeContext.Server.Web.Static.RedirectToDefault
    }

    # convert any middleware into valid hashtables
    $Middleware = @(ConvertTo-PodeMiddleware -Middleware $Middleware -PSSession $PSCmdlet.SessionState)

    # if an access name was supplied, setup access as middleware first to it's after auth middleware
    if (![string]::IsNullOrWhiteSpace($Access)) {
        if ([string]::IsNullOrWhiteSpace($Authentication)) {
            # Access requires Authentication to be supplied on Routes
            throw ($PodeLocale.accessRequiresAuthenticationOnRoutesExceptionMessage)
        }

        if (!(Test-PodeAccessExists -Name $Access)) {
            # Access method does not exist
            throw ($PodeLocale.accessMethodDoesNotExistExceptionMessage -f $Access)
        }

        $options = @{
            Name = $Access
        }

        $Middleware = (@(Get-PodeAccessMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware)
    }

    # if an auth name was supplied, setup the auth as the first middleware
    if (![string]::IsNullOrWhiteSpace($Authentication)) {
        if (!(Test-PodeAuthExists -Name $Authentication)) {
            # Authentication method does not exist
            throw ($PodeLocale.authenticationMethodDoesNotExistExceptionMessage)
        }

        $options = @{
            Name = $Authentication
            Anon = $AllowAnon
        }

        $Middleware = (@(Get-PodeAuthMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware)
    }

    # workout a default content type for the route
    $ContentType = Find-PodeRouteContentType -Path $Path -ContentType $ContentType

    # workout a default transfer encoding for the route
    $TransferEncoding = Find-PodeRouteTransferEncoding -Path $Path -TransferEncoding $TransferEncoding

    #The path use KleeneStar(Asterisk)
    $KleeneStar = $OrigPath.Contains('*')

    # add the route(s)
    Write-Verbose "Adding Route: [$($Method)] $($Path)"
    $newRoutes = @(foreach ($_endpoint in $endpoints) {
            @{
                Source            = $Source
                Path              = $Path
                KleeneStar        = $KleeneStar
                Method            = $Method
                Defaults          = $Defaults
                RedirectToDefault = $RedirectToDefault
                Middleware        = $Middleware
                Authentication    = $Authentication
                Access            = $Access
                AccessMeta        = @{
                    Role   = $Role
                    Group  = $Group
                    Scope  = $Scope
                    User   = $User
                    Custom = $CustomAccess
                }
                Endpoint          = @{
                    Protocol = $_endpoint.Protocol
                    Address  = $_endpoint.Address.Trim()
                    Name     = $_endpoint.Name
                }
                ContentType       = $ContentType
                TransferEncoding  = $TransferEncoding
                ErrorType         = $ErrorContentType
                Download          = $DownloadOnly
                IsStatic          = $true
                FileBrowser       = $FileBrowser.isPresent
                OpenApi           = @{
                    Path           = $OpenApiPath
                    Responses      = @{}
                    Parameters     = $null
                    RequestBody    = $null
                    CallBacks      = @{}
                    Authentication = @()
                    Servers        = @()
                    DefinitionTag  = $DefinitionTag
                }
                Metrics           = @{
                    Requests = @{
                        Total       = 0
                        StatusCodes = @{}
                    }
                }
            }
        })

    $PodeContext.Server.Routes[$Method][$Path] += @($newRoutes)

    # return the routes?
    if ($PassThru) {
        return $newRoutes
    }
}

<#
.SYNOPSIS
Adds a Signal Route for WebSockets.
 
.DESCRIPTION
Adds a Signal Route, with path, that when called with invoke any logic.
 
.PARAMETER Path
The URI path for the Signal Route.
 
.PARAMETER ScriptBlock
A ScriptBlock for the Signal Route's main logic.
 
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) this Signal Route should be bound against.
 
.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Signal Route's main logic.
 
.PARAMETER ArgumentList
An array of arguments to supply to the Signal Route's ScriptBlock.
 
.PARAMETER IfExists
Specifies what action to take when a Signal Route already exists. (Default: Default)
 
.EXAMPLE
Add-PodeSignalRoute -Path '/message' -ScriptBlock { /* logic */ }
 
.EXAMPLE
Add-PodeSignalRoute -Path '/message' -ScriptBlock { /* logic */ } -ArgumentList 'arg1', 'arg2'
#>

function Add-PodeSignalRoute {
    [CmdletBinding(DefaultParameterSetName = 'Script')]
    [OutputType([System.Object[]])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter(ParameterSetName = 'Script')]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [string[]]
        $EndpointName,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $FilePath,

        [Parameter()]
        [object[]]
        $ArgumentList,

        [Parameter()]
        [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
        [string]
        $IfExists = 'Default'
    )

    # check if we have any route group info defined
    if ($null -ne $RouteGroup) {
        if (![string]::IsNullOrWhiteSpace($RouteGroup.Path)) {
            $Path = "$($RouteGroup.Path)$($Path)"
        }

        if ([string]::IsNullOrWhiteSpace($EndpointName)) {
            $EndpointName = $RouteGroup.EndpointName
        }

        if ($RouteGroup.IfExists -ine 'default') {
            $IfExists = $RouteGroup.IfExists
        }
    }

    $Method = 'Signal'

    # store the original path
    $origPath = $Path

    # ensure the route has appropriate slashes
    $Path = Update-PodeRouteSlash -Path $Path

    # get endpoints from name
    $endpoints = Find-PodeEndpoint -EndpointName $EndpointName

    # get default route IfExists state
    if ($IfExists -ieq 'Default') {
        $IfExists = Get-PodeRouteIfExistsPreference
    }

    # ensure the route doesn't already exist for each endpoint
    $endpoints = @(foreach ($_endpoint in $endpoints) {
            $found = Test-PodeRouteInternal -Method $Method -Path $Path -Protocol $_endpoint.Protocol -Address $_endpoint.Address -ThrowError:($IfExists -ieq 'Error')

            if ($found) {
                if ($IfExists -ieq 'Overwrite') {
                    Remove-PodeSignalRoute -Path $origPath -EndpointName $_endpoint.Name
                }

                if ($IfExists -ieq 'Skip') {
                    continue
                }
            }

            $_endpoint
        })

    if (($null -eq $endpoints) -or ($endpoints.Length -eq 0)) {
        return
    }

    # if scriptblock and file path are all null/empty, error
    if ((Test-PodeIsEmpty $ScriptBlock) -and (Test-PodeIsEmpty $FilePath)) {
        # [Method] Path: No logic passed
        throw ($PodeLocale.noLogicPassedForMethodRouteExceptionMessage -f $Method, $Path)
    }

    # if we have a file path supplied, load that path as a scriptblock
    if ($PSCmdlet.ParameterSetName -ieq 'file') {
        $ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
    }

    # check for scoped vars
    $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

    # add the route(s)
    Write-Verbose "Adding Route: [$($Method)] $($Path)"
    $newRoutes = @(foreach ($_endpoint in $endpoints) {
            @{
                Logic          = $ScriptBlock
                UsingVariables = $usingVars
                Endpoint       = @{
                    Protocol = $_endpoint.Protocol
                    Address  = $_endpoint.Address.Trim()
                    Name     = $_endpoint.Name
                }
                Arguments      = $ArgumentList
                Method         = $Method
                Path           = $Path
                IsStatic       = $false
                Metrics        = @{
                    Requests = @{
                        Total = 0
                    }
                }
            }
        })

    $PodeContext.Server.Routes[$Method][$Path] += @($newRoutes)
}

<#
.SYNOPSIS
Add a Route Group for multiple Routes.
 
.DESCRIPTION
Add a Route Group for sharing values between multiple Routes.
 
.PARAMETER Path
The URI path to use as a base for the Routes, that should be prepended.
 
.PARAMETER Routes
A ScriptBlock for adding Routes.
 
.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware to give each Route.
 
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) to use for the Routes.
 
.PARAMETER ContentType
The content type to use for the Routes, when parsing any payloads.
 
.PARAMETER TransferEncoding
The transfer encoding to use for the Routes, when parsing any payloads.
 
.PARAMETER ErrorContentType
The content type of any error pages that may get returned.
 
.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on the Routes.
 
.PARAMETER Access
The name of an Access method which should be used as middleware on this Route.
 
.PARAMETER IfExists
Specifies what action to take when a Route already exists. (Default: Default)
 
.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER User
One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER AllowAnon
If supplied, the Routes will allow anonymous access for non-authenticated users.
 
.PARAMETER OADefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
 
.EXAMPLE
Add-PodeRouteGroup -Path '/api' -Routes { Add-PodeRoute -Path '/route1' -Etc }
#>

function Add-PodeRouteGroup {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $Routes,

        [Parameter()]
        [object[]]
        $Middleware,

        [Parameter()]
        [string[]]
        $EndpointName,

        [Parameter()]
        [string]
        $ContentType,

        [Parameter()]
        [ValidateSet('', 'gzip', 'deflate')]
        [string]
        $TransferEncoding,

        [Parameter()]
        [string]
        $ErrorContentType,

        [Parameter()]
        [Alias('Auth')]
        [string]
        $Authentication,

        [Parameter()]
        [string]
        $Access,

        [Parameter()]
        [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
        [string]
        $IfExists = 'Default',

        [Parameter()]
        [string[]]
        $Role,

        [Parameter()]
        [string[]]
        $Group,

        [Parameter()]
        [string[]]
        $Scope,

        [Parameter()]
        [string[]]
        $User,

        [switch]
        $AllowAnon,

        [string[]]
        $OADefinitionTag
    )

    if (Test-PodeIsEmpty $Routes) {
        # The Route parameter needs a valid, not empty, scriptblock
        throw ($PodeLocale.routeParameterNeedsValidScriptblockExceptionMessage)
    }

    if ($Path -eq '/') {
        $Path = $null
    }

    # check for scoped vars
    $Routes, $usingVars = Convert-PodeScopedVariables -ScriptBlock $Routes -PSSession $PSCmdlet.SessionState

    # group details
    if ($null -ne $RouteGroup) {
        if (![string]::IsNullOrWhiteSpace($RouteGroup.Path)) {
            $Path = "$($RouteGroup.Path)$($Path)"
        }

        if ($null -ne $RouteGroup.Middleware) {
            $Middleware = $RouteGroup.Middleware + $Middleware
        }

        if ([string]::IsNullOrWhiteSpace($EndpointName)) {
            $EndpointName = $RouteGroup.EndpointName
        }

        if ([string]::IsNullOrWhiteSpace($ContentType)) {
            $ContentType = $RouteGroup.ContentType
        }

        if ([string]::IsNullOrWhiteSpace($TransferEncoding)) {
            $TransferEncoding = $RouteGroup.TransferEncoding
        }

        if ([string]::IsNullOrWhiteSpace($ErrorContentType)) {
            $ErrorContentType = $RouteGroup.ErrorContentType
        }

        if ([string]::IsNullOrWhiteSpace($Authentication)) {
            $Authentication = $RouteGroup.Authentication
        }

        if ([string]::IsNullOrWhiteSpace($Access)) {
            $Access = $RouteGroup.Access
        }

        if ($RouteGroup.AllowAnon) {
            $AllowAnon = $RouteGroup.AllowAnon
        }

        if ($RouteGroup.IfExists -ine 'default') {
            $IfExists = $RouteGroup.IfExists
        }

        if ($null -ne $RouteGroup.AccessMeta.Role) {
            $Role = $RouteGroup.AccessMeta.Role + $Role
        }

        if ($null -ne $RouteGroup.AccessMeta.Group) {
            $Group = $RouteGroup.AccessMeta.Group + $Group
        }

        if ($null -ne $RouteGroup.AccessMeta.Scope) {
            $Scope = $RouteGroup.AccessMeta.Scope + $Scope
        }

        if ($null -ne $RouteGroup.AccessMeta.User) {
            $User = $RouteGroup.AccessMeta.User + $User
        }

        if ($null -ne $RouteGroup.AccessMeta.Custom) {
            $CustomAccess = $RouteGroup.AccessMeta.Custom
        }

        if ($null -ne $RouteGroup.OADefinitionTag ) {
            $OADefinitionTag = $RouteGroup.OADefinitionTag
        }

    }

    $RouteGroup = @{
        Path             = $Path
        Middleware       = $Middleware
        EndpointName     = $EndpointName
        ContentType      = $ContentType
        TransferEncoding = $TransferEncoding
        ErrorContentType = $ErrorContentType
        Authentication   = $Authentication
        Access           = $Access
        AllowAnon        = $AllowAnon
        IfExists         = $IfExists
        OADefinitionTag  = $OADefinitionTag
        AccessMeta       = @{
            Role   = $Role
            Group  = $Group
            Scope  = $Scope
            User   = $User
            Custom = $CustomAccess
        }
    }

    # add routes
    $null = Invoke-PodeScriptBlock -ScriptBlock $Routes -UsingVariables $usingVars -Splat -NoNewClosure
}

<#
.SYNOPSIS
Add a Static Route Group for multiple Static Routes.
 
.DESCRIPTION
Add a Static Route Group for sharing values between multiple Static Routes.
 
.PARAMETER Path
The URI path to use as a base for the Static Routes.
 
.PARAMETER Source
A literal, or relative, base path to the directory that contains the static content, that should be prepended.
 
.PARAMETER Routes
A ScriptBlock for adding Static Routes.
 
.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware to give each Static Route.
 
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) to use for the Static Routes.
 
.PARAMETER ContentType
The content type to use for the Static Routes, when parsing any payloads.
 
.PARAMETER TransferEncoding
The transfer encoding to use for the Static Routes, when parsing any payloads.
 
.PARAMETER Defaults
An array of default pages to display, such as 'index.html', for each Static Route.
 
.PARAMETER ErrorContentType
The content type of any error pages that may get returned.
 
.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on the Static Routes.
 
.PARAMETER Access
The name of an Access method which should be used as middleware on this Route.
 
.PARAMETER IfExists
Specifies what action to take when a Static Route already exists. (Default: Default)
 
.PARAMETER AllowAnon
If supplied, the Static Routes will allow anonymous access for non-authenticated users.
 
.PARAMETER FileBrowser
When supplied, If the path is a folder, instead of returning 404, will return A browsable content of the directory.
 
.PARAMETER DownloadOnly
When supplied, all static content on the Routes will be attached as downloads - rather than rendered.
 
.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER User
One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER RedirectToDefault
If supplied, the user will be redirected to the default page if found instead of the page being rendered as the folder path.
 
.EXAMPLE
Add-PodeStaticRouteGroup -Path '/static' -Routes { Add-PodeStaticRoute -Path '/images' -Etc }
#>

function Add-PodeStaticRouteGroup {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path,

        [Parameter()]
        [string]
        $Source,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $Routes,

        [Parameter()]
        [object[]]
        $Middleware,

        [Parameter()]
        [string[]]
        $EndpointName,

        [Parameter()]
        [string]
        $ContentType,

        [Parameter()]
        [ValidateSet('', 'gzip', 'deflate')]
        [string]
        $TransferEncoding,

        [Parameter()]
        [string[]]
        $Defaults,

        [Parameter()]
        [string]
        $ErrorContentType,

        [Parameter()]
        [Alias('Auth')]
        [string]
        $Authentication,

        [Parameter()]
        [string]
        $Access,

        [Parameter()]
        [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
        [string]
        $IfExists = 'Default',

        [Parameter()]
        [string[]]
        $Role,

        [Parameter()]
        [string[]]
        $Group,

        [Parameter()]
        [string[]]
        $Scope,

        [Parameter()]
        [string[]]
        $User,

        [switch]
        $AllowAnon,

        [switch]
        $FileBrowser,

        [switch]
        $DownloadOnly,

        [switch]
        $RedirectToDefault
    )

    if (Test-PodeIsEmpty $Routes) {
        # The Route parameter needs a valid, not empty, scriptblock
        throw ($PodeLocale.routeParameterNeedsValidScriptblockExceptionMessage)
    }

    if ($Path -eq '/') {
        $Path = $null
    }

    # check for scoped vars
    $Routes, $usingVars = Convert-PodeScopedVariables -ScriptBlock $Routes -PSSession $PSCmdlet.SessionState

    # group details
    if ($null -ne $RouteGroup) {
        if (![string]::IsNullOrWhiteSpace($RouteGroup.Path)) {
            $Path = "$($RouteGroup.Path)$($Path)"
        }

        if (![string]::IsNullOrWhiteSpace($RouteGroup.Source)) {
            $Source = [System.IO.Path]::Combine($Source, $RouteGroup.Source.TrimStart('\/'))
        }

        if ($null -ne $RouteGroup.Middleware) {
            $Middleware = $RouteGroup.Middleware + $Middleware
        }

        if ([string]::IsNullOrWhiteSpace($EndpointName)) {
            $EndpointName = $RouteGroup.EndpointName
        }

        if ([string]::IsNullOrWhiteSpace($ContentType)) {
            $ContentType = $RouteGroup.ContentType
        }

        if ([string]::IsNullOrWhiteSpace($TransferEncoding)) {
            $TransferEncoding = $RouteGroup.TransferEncoding
        }

        if ([string]::IsNullOrWhiteSpace($ErrorContentType)) {
            $ErrorContentType = $RouteGroup.ErrorContentType
        }

        if ([string]::IsNullOrWhiteSpace($Authentication)) {
            $Authentication = $RouteGroup.Authentication
        }

        if ([string]::IsNullOrWhiteSpace($Access)) {
            $Access = $RouteGroup.Access
        }

        if (Test-PodeIsEmpty $Defaults) {
            $Defaults = $RouteGroup.Defaults
        }

        if ($RouteGroup.AllowAnon) {
            $AllowAnon = $RouteGroup.AllowAnon
        }

        if ($RouteGroup.DownloadOnly) {
            $DownloadOnly = $RouteGroup.DownloadOnly
        }

        if ($RouteGroup.FileBrowser) {
            $FileBrowser = $RouteGroup.FileBrowser
        }

        if ($RouteGroup.RedirectToDefault) {
            $RedirectToDefault = $RouteGroup.RedirectToDefault
        }

        if ($RouteGroup.IfExists -ine 'default') {
            $IfExists = $RouteGroup.IfExists
        }

        if ($null -ne $RouteGroup.AccessMeta.Role) {
            $Role = $RouteGroup.AccessMeta.Role + $Role
        }

        if ($null -ne $RouteGroup.AccessMeta.Group) {
            $Group = $RouteGroup.AccessMeta.Group + $Group
        }

        if ($null -ne $RouteGroup.AccessMeta.Scope) {
            $Scope = $RouteGroup.AccessMeta.Scope + $Scope
        }

        if ($null -ne $RouteGroup.AccessMeta.User) {
            $User = $RouteGroup.AccessMeta.User + $User
        }

        if ($null -ne $RouteGroup.AccessMeta.Custom) {
            $CustomAccess = $RouteGroup.AccessMeta.Custom
        }
    }

    $RouteGroup = @{
        Path              = $Path
        Source            = $Source
        Middleware        = $Middleware
        EndpointName      = $EndpointName
        ContentType       = $ContentType
        TransferEncoding  = $TransferEncoding
        Defaults          = $Defaults
        RedirectToDefault = $RedirectToDefault
        ErrorContentType  = $ErrorContentType
        Authentication    = $Authentication
        Access            = $Access
        AllowAnon         = $AllowAnon
        DownloadOnly      = $DownloadOnly
        FileBrowser       = $FileBrowser
        IfExists          = $IfExists
        AccessMeta        = @{
            Role   = $Role
            Group  = $Group
            Scope  = $Scope
            User   = $User
            Custom = $CustomAccess
        }
    }

    # add routes
    $null = Invoke-PodeScriptBlock -ScriptBlock $Routes -UsingVariables $usingVars -Splat -NoNewClosure
}


<#
.SYNOPSIS
Adds a Signal Route Group for multiple WebSockets.
 
.DESCRIPTION
Adds a Signal Route Group for sharing values between multiple WebSockets.
 
.PARAMETER Path
The URI path to use as a base for the Signal Routes, that should be prepended.
 
.PARAMETER Routes
A ScriptBlock for adding Signal Routes.
 
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) to use for the Signal Routes.
 
.PARAMETER IfExists
Specifies what action to take when a Signal Route already exists. (Default: Default)
 
.EXAMPLE
Add-PodeSignalRouteGroup -Path '/signals' -Routes { Add-PodeSignalRoute -Path '/signal1' -Etc }
#>

function Add-PodeSignalRouteGroup {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $Routes,

        [Parameter()]
        [string[]]
        $EndpointName,

        [Parameter()]
        [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
        [string]
        $IfExists = 'Default'
    )

    if (Test-PodeIsEmpty $Routes) {
        # The Route parameter needs a valid, not empty, scriptblock
        throw ($PodeLocale.routeParameterNeedsValidScriptblockExceptionMessage)
    }

    if ($Path -eq '/') {
        $Path = $null
    }

    # check for scoped vars
    $Routes, $usingVars = Convert-PodeScopedVariables -ScriptBlock $Routes -PSSession $PSCmdlet.SessionState

    # group details
    if ($null -ne $RouteGroup) {
        if (![string]::IsNullOrWhiteSpace($RouteGroup.Path)) {
            $Path = "$($RouteGroup.Path)$($Path)"
        }

        if ([string]::IsNullOrWhiteSpace($EndpointName)) {
            $EndpointName = $RouteGroup.EndpointName
        }

        if ($RouteGroup.IfExists -ine 'default') {
            $IfExists = $RouteGroup.IfExists
        }
    }

    $RouteGroup = @{
        Path         = $Path
        EndpointName = $EndpointName
        IfExists     = $IfExists
    }

    # add routes
    $null = Invoke-PodeScriptBlock -ScriptBlock $Routes -UsingVariables $usingVars -Splat -NoNewClosure
}

<#
.SYNOPSIS
Remove a specific Route.
 
.DESCRIPTION
Remove a specific Route.
 
.PARAMETER Method
The method of the Route to remove.
 
.PARAMETER Path
The path of the Route to remove.
 
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) bound to the Route to be removed.
 
.EXAMPLE
Remove-PodeRoute -Method Get -Route '/about'
 
.EXAMPLE
Remove-PodeRoute -Method Post -Route '/users/:userId' -EndpointName User
#>

function Remove-PodeRoute {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
        [string]
        $Method,

        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $EndpointName
    )

    # split route on '?' for query
    $Path = Split-PodeRouteQuery -Path $Path

    # ensure the route has appropriate slashes and replace parameters
    $Path = Update-PodeRouteSlash -Path $Path
    $Path = Resolve-PodePlaceholder -Path $Path

    # ensure route does exist
    if (!$PodeContext.Server.Routes[$Method].Contains($Path)) {
        return
    }

    # select the candidate route for deletion
    $route = @($PodeContext.Server.Routes[$Method][$Path] | Where-Object {
            $_.Endpoint.Name -ieq $EndpointName
        })

    foreach ($r in $route) {
        # remove the operationId from the openapi operationId list
        if ($r.OpenAPI) {
            foreach ( $tag in $r.OpenAPI.DefinitionTag) {
                if ($tag -and ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId -ccontains $route.OpenAPI.OperationId)) {
                    $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId = $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId | Where-Object { $_ -ne $route.OpenAPI.OperationId }
                }
            }
        }
    }

    # remove the route's logic
    $PodeContext.Server.Routes[$Method][$Path] = @($PodeContext.Server.Routes[$Method][$Path] | Where-Object {
            $_.Endpoint.Name -ine $EndpointName
        })

    # if the route has no more logic, just remove it
    if ((Get-PodeCount $PodeContext.Server.Routes[$Method][$Path]) -eq 0) {
        $null = $PodeContext.Server.Routes[$Method].Remove($Path)
    }
}

<#
.SYNOPSIS
Remove a specific static Route.
 
.DESCRIPTION
Remove a specific static Route.
 
.PARAMETER Path
The path of the static Route to remove.
 
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) bound to the static Route to be removed.
 
.EXAMPLE
Remove-PodeStaticRoute -Path '/assets'
#>

function Remove-PodeStaticRoute {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $EndpointName
    )

    $Method = 'Static'

    # ensure the route has appropriate slashes and replace parameters
    $Path = Update-PodeRouteSlash -Path $Path -Static

    # ensure route does exist
    if (!$PodeContext.Server.Routes[$Method].Contains($Path)) {
        return
    }

    # remove the route's logic
    $PodeContext.Server.Routes[$Method][$Path] = @($PodeContext.Server.Routes[$Method][$Path] | Where-Object {
            $_.Endpoint.Name -ine $EndpointName
        })

    # if the route has no more logic, just remove it
    if ((Get-PodeCount $PodeContext.Server.Routes[$Method][$Path]) -eq 0) {
        $null = $PodeContext.Server.Routes[$Method].Remove($Path)
    }
}

<#
.SYNOPSIS
Remove a specific Signal Route.
 
.DESCRIPTION
Remove a specific Signal Route.
 
.PARAMETER Path
The path of the Signal Route to remove.
 
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) bound to the Signal Route to be removed.
 
.EXAMPLE
Remove-PodeSignalRoute -Route '/message'
#>

function Remove-PodeSignalRoute {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $EndpointName
    )

    $Method = 'Signal'

    # ensure the route has appropriate slashes and replace parameters
    $Path = Update-PodeRouteSlash -Path $Path

    # ensure route does exist
    if (!$PodeContext.Server.Routes[$Method].Contains($Path)) {
        return
    }

    # remove the route's logic
    $PodeContext.Server.Routes[$Method][$Path] = @($PodeContext.Server.Routes[$Method][$Path] | Where-Object {
            $_.Endpoint.Name -ine $EndpointName
        })

    # if the route has no more logic, just remove it
    if ((Get-PodeCount $PodeContext.Server.Routes[$Method][$Path]) -eq 0) {
        $null = $PodeContext.Server.Routes[$Method].Remove($Path)
    }
}

<#
.SYNOPSIS
Removes all added Routes, or Routes for a specific Method.
 
.DESCRIPTION
Removes all added Routes, or Routes for a specific Method.
 
.PARAMETER Method
The Method to from which to remove all Routes.
 
.EXAMPLE
Clear-PodeRoutes
 
.EXAMPLE
Clear-PodeRoutes -Method Get
#>

function Clear-PodeRoutes {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('', 'Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
        [string]
        $Method
    )

    if (![string]::IsNullOrWhiteSpace($Method)) {
        $PodeContext.Server.Routes[$Method].Clear()
    }
    else {
        $PodeContext.Server.Routes.Keys.Clone() | ForEach-Object {
            $PodeContext.Server.Routes[$_].Clear()
        }
    }
}

<#
.SYNOPSIS
Removes all added static Routes.
 
.DESCRIPTION
Removes all added static Routes.
 
.EXAMPLE
Clear-PodeStaticRoutes
#>

function Clear-PodeStaticRoutes {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
    [CmdletBinding()]
    param()

    $PodeContext.Server.Routes['Static'].Clear()
}

<#
.SYNOPSIS
Removes all added Signal Routes.
 
.DESCRIPTION
Removes all added Signal Routes.
 
.EXAMPLE
Clear-PodeSignalRoutes
#>

function Clear-PodeSignalRoutes {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
    [CmdletBinding()]
    param()

    $PodeContext.Server.Routes['Signal'].Clear()
}

<#
.SYNOPSIS
Takes an array of Commands, or a Module, and converts them into Routes.
 
.DESCRIPTION
Takes an array of Commands (Functions/Aliases), or a Module, and generates appropriate Routes for the commands.
 
.PARAMETER Commands
An array of Commands to convert - if a Module is supplied, these Commands must be present within that Module.
 
.PARAMETER Module
A Module whose exported commands will be converted.
 
.PARAMETER Method
An override HTTP method to use when generating the Routes. If not supplied, Pode will make a best guess based on the Command's Verb.
 
.PARAMETER Path
An optional Path for the Route, to prepend before the Command Name and Module.
 
.PARAMETER Middleware
Like normal Routes, an array of Middleware that will be applied to all generated Routes.
 
.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on this Route.
 
.PARAMETER Access
The name of an Access method which should be used as middleware on this Route.
 
.PARAMETER AllowAnon
If supplied, the Route will allow anonymous access for non-authenticated users.
 
.PARAMETER NoVerb
If supplied, the Command's Verb will not be included in the Route's path.
 
.PARAMETER NoOpenApi
If supplied, no OpenAPI definitions will be generated for the routes created.
 
.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER User
One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method.
 
.EXAMPLE
ConvertTo-PodeRoute -Commands @('Get-ChildItem', 'Get-Host', 'Invoke-Expression') -Middleware { ... }
 
.EXAMPLE
ConvertTo-PodeRoute -Commands @('Get-ChildItem', 'Get-Host', 'Invoke-Expression') -Authentication AuthName
 
.EXAMPLE
ConvertTo-PodeRoute -Module Pester -Path '/api'
 
.EXAMPLE
ConvertTo-PodeRoute -Commands @('Invoke-Pester') -Module Pester
#>

function ConvertTo-PodeRoute {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline = $true, Position = 0 )]
        [string[]]
        $Commands,

        [Parameter()]
        [string]
        $Module,

        [Parameter()]
        [ValidateSet('', 'Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace')]
        [string]
        $Method,

        [Parameter()]
        [string]
        $Path = '/',

        [Parameter()]
        [object[]]
        $Middleware,

        [Parameter()]
        [Alias('Auth')]
        [string]
        $Authentication,

        [Parameter()]
        [string]
        $Access,

        [Parameter()]
        [string[]]
        $Role,

        [Parameter()]
        [string[]]
        $Group,

        [Parameter()]
        [string[]]
        $Scope,

        [Parameter()]
        [string[]]
        $User,

        [switch]
        $AllowAnon,

        [switch]
        $NoVerb,

        [switch]
        $NoOpenApi
    )
    begin {
        # Initialize an array to hold piped-in values
        $pipelineValue = @()
    }

    process {
        # Add the current piped-in value to the array
        $pipelineValue += $_
    }

    end {
        # Set InputObject to the array of values
        if ($pipelineValue.Count -gt 1) {
            $Commands = $pipelineValue
        }

        # if a module was supplied, import it - then validate the commands
        if (![string]::IsNullOrWhiteSpace($Module)) {
            Import-PodeModule -Name $Module

            Write-Verbose 'Getting exported commands from module'
            $ModuleCommands = (Get-Module -Name $Module | Sort-Object -Descending | Select-Object -First 1).ExportedCommands.Keys

            # if commands were supplied validate them - otherwise use all exported ones
            if (Test-PodeIsEmpty $Commands) {
                Write-Verbose "Using all commands in $($Module) for converting to routes"
                $Commands = $ModuleCommands
            }
            else {
                Write-Verbose "Validating supplied commands against module's exported commands"
                foreach ($cmd in $Commands) {
                    if ($ModuleCommands -inotcontains $cmd) {
                        # Module Module does not contain function cmd to convert to a Route
                        throw ($PodeLocale.moduleDoesNotContainFunctionExceptionMessage -f $Module, $cmd)
                    }
                }
            }
        }

        # if there are no commands, fail
        if (Test-PodeIsEmpty $Commands) {
            # No commands supplied to convert to Routes
            throw ($PodeLocale.noCommandsSuppliedToConvertToRoutesExceptionMessage)
        }

        # trim end trailing slashes from the path
        $Path = Protect-PodeValue -Value $Path -Default '/'
        $Path = $Path.TrimEnd('/')

        # create the routes for each of the commands
        foreach ($cmd in $Commands) {
            # get module verb/noun and comvert verb to HTTP method
            $split = ($cmd -split '\-')

            if ($split.Length -ge 2) {
                $verb = $split[0]
                $noun = $split[1..($split.Length - 1)] -join ([string]::Empty)
            }
            else {
                $verb = [string]::Empty
                $noun = $split[0]
            }

            # determine the http method, or use the one passed
            $_method = $Method
            if ([string]::IsNullOrWhiteSpace($_method)) {
                $_method = Convert-PodeFunctionVerbToHttpMethod -Verb $verb
            }

            # use the full function name, or remove the verb
            $name = $cmd
            if ($NoVerb) {
                $name = $noun
            }

            # build the route's path
            $_path = ("$($Path)/$($Module)/$($name)" -replace '[/]+', '/')

            # create the route
            $params = @{
                Method         = $_method
                Path           = $_path
                Middleware     = $Middleware
                Authentication = $Authentication
                Access         = $Access
                Role           = $Role
                Group          = $Group
                Scope          = $Scope
                User           = $User
                AllowAnon      = $AllowAnon
                ArgumentList   = $cmd
                PassThru       = $true
            }

            $route = Add-PodeRoute @params -ScriptBlock {
                param($cmd)

                # either get params from the QueryString or Payload
                if ($WebEvent.Method -ieq 'get') {
                    $parameters = $WebEvent.Query
                }
                else {
                    $parameters = $WebEvent.Data
                }

                # invoke the function
                $result = (. $cmd @parameters)

                # if we have a result, convert it to json
                if (!(Test-PodeIsEmpty $result)) {
                    Write-PodeJsonResponse -Value $result -Depth 1
                }
            }

            # set the openapi metadata of the function, unless told to skip
            if ($NoOpenApi) {
                continue
            }

            $help = Get-Help -Name $cmd
            $route = ($route | Set-PodeOARouteInfo -Summary $help.Synopsis -Tags $Module -PassThru)

            # set the routes parameters (get = query, everything else = payload)
            $params = (Get-Command -Name $cmd).Parameters
            if (($null -eq $params) -or ($params.Count -eq 0)) {
                continue
            }

            $props = @(foreach ($key in $params.Keys) {
                    $params[$key] | ConvertTo-PodeOAPropertyFromCmdletParameter
                })

            if ($_method -ieq 'get') {
                $route | Set-PodeOARequest -Parameters @(foreach ($prop in $props) { $prop | ConvertTo-PodeOAParameter -In Query })
            }

            else {
                $route | Set-PodeOARequest -RequestBody (
                    New-PodeOARequestBody -ContentSchemas @{ 'application/json' = (New-PodeOAObjectProperty -Array -Properties $props) }
                )
            }
        }
    }
}

<#
.SYNOPSIS
Helper function to generate simple GET routes.
 
.DESCRIPTION
Helper function to generate simple GET routes from ScritpBlocks, Files, and Views.
The output is always rendered as HTML.
 
.PARAMETER Name
A unique name for the page, that will be used in the Path for the route.
 
.PARAMETER ScriptBlock
A ScriptBlock to invoke, where any results will be converted to HTML.
 
.PARAMETER FilePath
A FilePath, literal or relative, to a valid HTML file.
 
.PARAMETER View
The name of a View to render, this can be HTML or Dynamic.
 
.PARAMETER Data
A hashtable of Data to supply to a Dynamic File/View, or to be splatted as arguments for the ScriptBlock.
 
.PARAMETER Path
An optional Path for the Route, to prepend before the Name.
 
.PARAMETER Middleware
Like normal Routes, an array of Middleware that will be applied to all generated Routes.
 
.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on this Route.
 
.PARAMETER Access
The name of an Access method which should be used as middleware on this Route.
 
.PARAMETER AllowAnon
If supplied, the Page will allow anonymous access for non-authenticated users.
 
.PARAMETER FlashMessages
If supplied, Views will have any flash messages supplied to them for rendering.
 
.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.
 
.PARAMETER User
One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method.
 
.EXAMPLE
Add-PodePage -Name Services -ScriptBlock { Get-Service }
 
.EXAMPLE
Add-PodePage -Name Index -View 'index'
 
.EXAMPLE
Add-PodePage -Name About -FilePath '.\views\about.pode' -Data @{ Date = [DateTime]::UtcNow }
#>

function Add-PodePage {
    [CmdletBinding(DefaultParameterSetName = 'ScriptBlock')]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'ScriptBlock')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $FilePath,

        [Parameter(Mandatory = $true, ParameterSetName = 'View')]
        [string]
        $View,

        [Parameter()]
        [hashtable]
        $Data,

        [Parameter()]
        [string]
        $Path = '/',

        [Parameter()]
        [object[]]
        $Middleware,

        [Parameter()]
        [Alias('Auth')]
        [string]
        $Authentication,

        [Parameter()]
        [string]
        $Access,

        [Parameter()]
        [string[]]
        $Role,

        [Parameter()]
        [string[]]
        $Group,

        [Parameter()]
        [string[]]
        $Scope,

        [Parameter()]
        [string[]]
        $User,

        [switch]
        $AllowAnon,

        [Parameter(ParameterSetName = 'View')]
        [switch]
        $FlashMessages
    )

    $logic = $null
    $arg = $null

    # ensure the name is a valid alphanumeric
    if ($Name -inotmatch '^[a-z0-9\-_]+$') {
        # The Page name should be a valid AlphaNumeric value
        throw ($PodeLocale.pageNameShouldBeAlphaNumericExceptionMessage -f $Name)
    }

    # trim end trailing slashes from the path
    $Path = Protect-PodeValue -Value $Path -Default '/'
    $Path = $Path.TrimEnd('/')

    # define the appropriate logic
    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'scriptblock' {
            if (Test-PodeIsEmpty $ScriptBlock) {
                # A non-empty ScriptBlock is required to create a Page Route
                throw ($PodeLocale.nonEmptyScriptBlockRequiredForPageRouteExceptionMessage)
            }

            $arg = @($ScriptBlock, $Data)
            $logic = {
                param($script, $data)

                # invoke the function (optional splat data)
                if (Test-PodeIsEmpty $data) {
                    $result = Invoke-PodeScriptBlock -ScriptBlock $script -Return
                }
                else {
                    $result = Invoke-PodeScriptBlock -ScriptBlock $script -Arguments $data -Return
                }

                # if we have a result, convert it to html
                if (!(Test-PodeIsEmpty $result)) {
                    Write-PodeHtmlResponse -Value $result
                }
            }
        }

        'file' {
            $FilePath = Get-PodeRelativePath -Path $FilePath -JoinRoot -TestPath
            $arg = @($FilePath, $Data)
            $logic = {
                param($file, $data)
                Write-PodeFileResponse -Path $file -ContentType 'text/html' -Data $data
            }
        }

        'view' {
            $arg = @($View, $Data, $FlashMessages)
            $logic = {
                param($view, $data, [bool]$flash)
                Write-PodeViewResponse -Path $view -Data $data -FlashMessages:$flash
            }
        }
    }

    # build the route's path
    $_path = ("$($Path)/$($Name)" -replace '[/]+', '/')

    # create the route
    $params = @{
        Method         = 'Get'
        Path           = $_path
        Middleware     = $Middleware
        Authentication = $Authentication
        Access         = $Access
        Role           = $Role
        Group          = $Group
        Scope          = $Scope
        User           = $User
        AllowAnon      = $AllowAnon
        ArgumentList   = $arg
        ScriptBlock    = $logic
    }

    Add-PodeRoute @params
}

<#
.SYNOPSIS
Get a Route(s).
 
.DESCRIPTION
Get a Route(s).
 
.PARAMETER Method
A Method to filter the routes.
 
.PARAMETER Path
A Path to filter the routes.
 
.PARAMETER EndpointName
The name of an endpoint to filter routes.
 
.EXAMPLE
Get-PodeRoute -Method Get -Path '/about'
 
.EXAMPLE
Get-PodeRoute -Method Post -Path '/users/:userId' -EndpointName User
#>

function Get-PodeRoute {
    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param(
        [Parameter()]
        [ValidateSet('', 'Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
        [string]
        $Method,

        [Parameter()]
        [string]
        $Path,

        [Parameter()]
        [string[]]
        $EndpointName
    )

    # start off with every route
    $routes = @()
    foreach ($route in $PodeContext.Server.Routes.Values.Values) {
        $routes += $route
    }

    # if we have a method, filter
    if (![string]::IsNullOrWhiteSpace($Method)) {
        $routes = @(foreach ($route in $routes) {
                if ($route.Method -ine $Method) {
                    continue
                }

                $route
            })
    }

    # if we have a path, filter
    if (![string]::IsNullOrWhiteSpace($Path)) {
        $Path = Split-PodeRouteQuery -Path $Path
        $Path = Update-PodeRouteSlash -Path $Path
        $Path = Resolve-PodePlaceholder -Path $Path

        $routes = @(foreach ($route in $routes) {
                if ($route.Path -ine $Path) {
                    continue
                }

                $route
            })
    }

    # further filter by endpoint names
    if (($null -ne $EndpointName) -and ($EndpointName.Length -gt 0)) {
        $routes = @(foreach ($name in $EndpointName) {
                foreach ($route in $routes) {
                    if ($route.Endpoint.Name -ine $name) {
                        continue
                    }

                    $route
                }
            })
    }

    # return
    return $routes
}

<#
.SYNOPSIS
Get a static Route(s).
 
.DESCRIPTION
Get a static Route(s).
 
.PARAMETER Path
A Path to filter the static routes.
 
.PARAMETER EndpointName
The name of an endpoint to filter static routes.
 
.EXAMPLE
Get-PodeStaticRoute -Path '/assets'
 
.EXAMPLE
Get-PodeStaticRoute -Path '/assets' -EndpointName User
#>

function Get-PodeStaticRoute {
    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param(
        [Parameter()]
        [string]
        $Path,

        [Parameter()]
        [string[]]
        $EndpointName
    )

    # start off with every route
    $routes = @()
    foreach ($route in $PodeContext.Server.Routes['Static'].Values) {
        $routes += $route
    }

    # if we have a path, filter
    if (![string]::IsNullOrWhiteSpace($Path)) {
        $Path = Update-PodeRouteSlash -Path $Path -Static
        $routes = @(foreach ($route in $routes) {
                if ($route.Path -ine $Path) {
                    continue
                }

                $route
            })
    }

    # further filter by endpoint names
    if (($null -ne $EndpointName) -and ($EndpointName.Length -gt 0)) {
        $routes = @(foreach ($name in $EndpointName) {
                foreach ($route in $routes) {
                    if ($route.Endpoint.Name -ine $name) {
                        continue
                    }

                    $route
                }
            })
    }

    # return
    return $routes
}

<#
.SYNOPSIS
Get a Signal Route(s).
 
.DESCRIPTION
Get a Signal Route(s).
 
.PARAMETER Path
A Path to filter the signal routes.
 
.PARAMETER EndpointName
The name of an endpoint to filter signal routes.
 
.EXAMPLE
Get-PodeSignalRoute -Path '/message'
#>

function Get-PodeSignalRoute {
    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param(
        [Parameter()]
        [string]
        $Path,

        [Parameter()]
        [string[]]
        $EndpointName
    )

    # start off with every route
    $routes = @()
    foreach ($route in $PodeContext.Server.Routes['Signal'].Values) {
        $routes += $route
    }

    # if we have a path, filter
    if (![string]::IsNullOrWhiteSpace($Path)) {
        $Path = Update-PodeRouteSlash -Path $Path
        $routes = @(foreach ($route in $routes) {
                if ($route.Path -ine $Path) {
                    continue
                }

                $route
            })
    }

    # further filter by endpoint names
    if (($null -ne $EndpointName) -and ($EndpointName.Length -gt 0)) {
        $routes = @(foreach ($name in $EndpointName) {
                foreach ($route in $routes) {
                    if ($route.Endpoint.Name -ine $name) {
                        continue
                    }

                    $route
                }
            })
    }

    # return
    return $routes
}

<#
.SYNOPSIS
Automatically loads route ps1 files
 
.DESCRIPTION
Automatically loads route ps1 files from either a /routes folder, or a custom folder. Saves space dot-sourcing them all one-by-one.
 
.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.
 
.PARAMETER IfExists
Specifies what action to take when a Route already exists. (Default: Default)
 
.EXAMPLE
Use-PodeRoutes
 
.EXAMPLE
Use-PodeRoutes -Path './my-routes' -IfExists Skip
#>

function Use-PodeRoutes {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path,

        [Parameter()]
        [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
        [string]
        $IfExists = 'Default'
    )

    if ($IfExists -ieq 'Default') {
        $IfExists = Get-PodeRouteIfExistsPreference
    }

    Use-PodeFolder -Path $Path -DefaultPath 'routes'
}

<#
.SYNOPSIS
Set the default IfExists preference for Routes.
 
.DESCRIPTION
Set the default IfExists preference for Routes.
 
.PARAMETER Value
Specifies what action to take when a Route already exists. (Default: Default)
 
.EXAMPLE
Set-PodeRouteIfExistsPreference -Value Overwrite
#>

function Set-PodeRouteIfExistsPreference {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
        [string]
        $Value = 'Default'
    )

    $PodeContext.Server.Preferences.Routes.IfExists = $Value
}

<#
.SYNOPSIS
Test if a Route already exists.
 
.DESCRIPTION
Test if a Route already exists for a given Method and Path.
 
.PARAMETER Method
The HTTP Method of the Route.
 
.PARAMETER Path
The URI path of the Route.
 
.PARAMETER EndpointName
The EndpointName of an Endpoint the Route is bound against.
 
.PARAMETER CheckWildcard
If supplied, Pode will check for the Route on the Method first, and then check for the Route on the '*' Method.
 
.EXAMPLE
Test-PodeRoute -Method Post -Path '/example'
 
.EXAMPLE
Test-PodeRoute -Method Post -Path '/example' -CheckWildcard
 
.EXAMPLE
Test-PodeRoute -Method Get -Path '/example/:exampleId' -CheckWildcard
#>

function Test-PodeRoute {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
        [string]
        $Method,

        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $EndpointName,

        [switch]
        $CheckWildcard
    )

    # split route on '?' for query
    $Path = Split-PodeRouteQuery -Path $Path
    if ([string]::IsNullOrWhiteSpace($Path)) {
        # No Path supplied for the Route
        throw ($PodeLocale.noPathSuppliedForRouteExceptionMessage)
    }

    # ensure the route has appropriate slashes
    $Path = Update-PodeRouteSlash -Path $Path
    $Path = Resolve-PodePlaceholder -Path $Path

    # get endpoint from name
    $endpoint = @(Find-PodeEndpoint -EndpointName $EndpointName)[0]

    # check for routes
    $found = (Test-PodeRouteInternal -Method $Method -Path $Path -Protocol $endpoint.Protocol -Address $endpoint.Address)
    if (!$found -and $CheckWildcard) {
        $found = (Test-PodeRouteInternal -Method '*' -Path $Path -Protocol $endpoint.Protocol -Address $endpoint.Address)
    }

    return $found
}

<#
.SYNOPSIS
Test if a Static Route already exists.
 
.DESCRIPTION
Test if a Static Route already exists for a given Path.
 
.PARAMETER Path
The URI path of the Static Route.
 
.PARAMETER EndpointName
The EndpointName of an Endpoint the Static Route is bound against.
 
.EXAMPLE
Test-PodeStaticRoute -Path '/assets'
#>

function Test-PodeStaticRoute {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $EndpointName
    )

    # store the route method
    $Method = 'Static'

    # split route on '?' for query
    $Path = Split-PodeRouteQuery -Path $Path
    if ([string]::IsNullOrWhiteSpace($Path)) {
        # No Path supplied for the Route
        throw ($PodeLocale.noPathSuppliedForRouteExceptionMessage)
    }

    # ensure the route has appropriate slashes
    $Path = Update-PodeRouteSlash -Path $Path -Static
    $Path = Resolve-PodePlaceholder -Path $Path

    # get endpoint from name
    $endpoint = @(Find-PodeEndpoint -EndpointName $EndpointName)[0]

    # check for routes
    return (Test-PodeRouteInternal -Method $Method -Path $Path -Protocol $endpoint.Protocol -Address $endpoint.Address)
}

<#
.SYNOPSIS
Test if a Signal Route already exists.
 
.DESCRIPTION
Test if a Signal Route already exists for a given Path.
 
.PARAMETER Path
The URI path of the Signal Route.
 
.PARAMETER EndpointName
The EndpointName of an Endpoint the Signal Route is bound against.
 
.EXAMPLE
Test-PodeSignalRoute -Path '/message'
#>

function Test-PodeSignalRoute {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $EndpointName
    )

    $Method = 'Signal'

    # ensure the route has appropriate slashes
    $Path = Update-PodeRouteSlash -Path $Path

    # get endpoint from name
    $endpoint = @(Find-PodeEndpoint -EndpointName $EndpointName)[0]

    # check for routes
    return (Test-PodeRouteInternal -Method $Method -Path $Path -Protocol $endpoint.Protocol -Address $endpoint.Address)
}