Public/OpenApi.ps1

<#
.SYNOPSIS
Enables the OpenAPI default route in Pode.
 
.DESCRIPTION
Enables the OpenAPI default route in Pode, as well as setting up details like Title and API Version.
 
.PARAMETER Path
An optional custom route path to access the OpenAPI definition. (Default: /openapi)
 
.PARAMETER Title
The Title of the API.
 
.PARAMETER Version
The Version of the API. (Default: 0.0.0)
 
.PARAMETER Description
A Description of the API.
 
.PARAMETER RouteFilter
An optional route filter for routes that should be included in the definition. (Default: /*)
 
.PARAMETER Middleware
Like normal Routes, an array of Middleware that will be applied to the route.
 
.PARAMETER RestrictRoutes
If supplied, only routes that are available on the Requests URI will be used to generate the OpenAPI definition.
 
.EXAMPLE
Enable-PodeOpenApi -Title 'My API' -Version '1.0.0' -RouteFilter '/api/*'
 
.EXAMPLE
Enable-PodeOpenApi -Title 'My API' -Version '1.0.0' -RouteFilter '/api/*' -RestrictRoutes
 
.EXAMPLE
Enable-PodeOpenApi -Path '/docs/openapi' -Title 'My API' -Version '1.0.0'
#>

function Enable-PodeOpenApi
{
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $Path = '/openapi',

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

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $Version = '0.0.0',

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $RouteFilter = '/*',

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

        [switch]
        $RestrictRoutes
    )

    # initialise openapi info
    $PodeContext.Server.OpenAPI.Title = $Title
    $PodeContext.Server.OpenAPI.Path = $Path

    $meta = @{
        Version = $Version
        Description = $Description
        RouteFilter = $RouteFilter
        RestrictRoutes = $RestrictRoutes
    }

    # add the OpenAPI route
    Add-PodeRoute -Method Get -Path $Path -ArgumentList $meta -Middleware $Middleware -ScriptBlock {
        param($e, $meta)
        $strict = $meta.RestrictRoutes

        # generate the openapi definition
        $def = Get-PodeOpenApiDefinitionInternal `
            -Title $PodeContext.Server.OpenAPI.Title `
            -Version $meta.Version `
            -Description $meta.Description `
            -RouteFilter $meta.RouteFilter `
            -Protocol $e.Protocol `
            -Endpoint $e.Endpoint `
            -RestrictRoutes:$strict

        # write the openapi definition
        Write-PodeJsonResponse -Value $def -Depth 20
    }
}

<#
.SYNOPSIS
Gets the OpenAPI definition.
 
.DESCRIPTION
Gets the OpenAPI definition for custom use in routes, or other functions.
 
.PARAMETER Title
The Title of the API. (Default: the title supplied in Enable-PodeOpenApi)
 
.PARAMETER Version
The Version of the API. (Default: the version supplied in Enable-PodeOpenApi)
 
.PARAMETER Description
A Description of the API. (Default: the description supplied into Enable-PodeOpenApi)
 
.PARAMETER RouteFilter
An optional route filter for routes that should be included in the definition. (Default: /*)
 
.PARAMETER RestrictRoutes
If supplied, only routes that are available on the Requests URI will be used to generate the OpenAPI definition.
 
.EXAMPLE
$def = Get-PodeOpenApiDefinition -RouteFilter '/api/*'
#>

function Get-PodeOpenApiDefinition
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $Title,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $Version,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $RouteFilter = '/*',

        [switch]
        $RestrictRoutes
    )

    $Title = Protect-PodeValue -Value $Title -Default $PodeContext.Server.OpenAPI.Title
    $Version = Protect-PodeValue -Value $Version -Default $PodeContext.Server.OpenAPI.Version
    $Description = Protect-PodeValue -Value $Description -Default $PodeContext.Server.OpenAPI.Description

    # generate the openapi definition
    return (Get-PodeOpenApiDefinitionInternal `
        -Title $Title `
        -Version $Version `
        -Description $Description `
        -RouteFilter $RouteFilter `
        -Protocol $WebEvent.Protocol `
        -Endpoint $WebEvent.Endpoint `
        -RestrictRoutes:$RestrictRoutes)
}

<#
.SYNOPSIS
Adds a response definition to the supplied route.
 
.DESCRIPTION
Adds a response definition to the supplied route.
 
.PARAMETER Route
The route to add the response definition, usually from -PassThru on Add-PodeRoute.
 
.PARAMETER StatusCode
The HTTP StatusCode for the response.
 
.PARAMETER ContentSchemas
The content-types and schema the response returns (the schema is created using the Property functions).
 
.PARAMETER HeaderSchemas
The header name and schema the response returns (the schema is created using the Property functions).
 
.PARAMETER Description
A Description of the response. (Default: the HTTP StatusCode description)
 
.PARAMETER Reference
A Reference Name of an existing component response to use.
 
.PARAMETER Default
If supplied, the response will be used as a default response - this overrides the StatusCode supplied.
 
.PARAMETER PassThru
If supplied, the route passed in will be returned for further chaining.
 
.EXAMPLE
Add-PodeRoute -PassThru | Add-PodeOAResponse -StatusCode 200 -ContentSchemas @{ 'application/json' = (New-PodeOAIntProperty -Name 'userId' -Object) }
 
.EXAMPLE
Add-PodeRoute -PassThru | Add-PodeOAResponse -StatusCode 200 -ContentSchemas @{ 'application/json' = 'UserIdSchema' }
 
.EXAMPLE
Add-PodeRoute -PassThru | Add-PodeOAResponse -StatusCode 200 -Reference 'OKResponse'
#>

function Add-PodeOAResponse
{
    [CmdletBinding(DefaultParameterSetName='Schema')]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [ValidateNotNullOrEmpty()]
        [hashtable[]]
        $Route,

        [Parameter(Mandatory=$true)]
        [int]
        $StatusCode,

        [Parameter(ParameterSetName='Schema')]
        [hashtable]
        $ContentSchemas,

        [Parameter(ParameterSetName='Schema')]
        [hashtable]
        $HeaderSchemas,

        [Parameter(ParameterSetName='Schema')]
        [string]
        $Description = $null,

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

        [switch]
        $Default,

        [switch]
        $PassThru
    )

    # set a general description for the status code
    if (!$Default -and [string]::IsNullOrWhiteSpace($Description)) {
        $Description = Get-PodeStatusDescription -StatusCode $StatusCode
    }

    # override status code with default
    $code = "$($StatusCode)"
    if ($Default) {
        $code = 'default'
    }

    # schemas or component reference?
    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'schema' {
            # build any content-type schemas
            $content = $null
            if ($null -ne $ContentSchemas) {
                $content = ($ContentSchemas | ConvertTo-PodeOAContentTypeSchema)
            }

            # build any header schemas
            $headers = $null
            if ($null -ne $HeaderSchemas) {
                $headers = ($HeaderSchemas | ConvertTo-PodeOAHeaderSchema)
            }
        }

        'reference' {
            if (!(Test-PodeOAComponentResponse -Name $Reference)) {
                throw "The OpenApi component response doesn't exist: $($Reference)"
            }
        }
    }

    # add the respones to the routes
    foreach ($r in @($Route)) {
        switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
            'schema' {
                $r.OpenApi.Responses[$code] = @{
                    description = $Description
                    content = $content
                    headers = $headers
                }
            }

            'reference' {
                $r.OpenApi.Responses[$code] = @{
                    '$ref' = "#/components/responses/$($Reference)"
                }
            }
        }
    }

    if ($PassThru) {
        return $Route
    }
}

<#
.SYNOPSIS
Adds a reusable component for responses.
 
.DESCRIPTION
Adds a reusable component for responses.
 
.PARAMETER Name
The reference Name of the response.
 
.PARAMETER ContentSchemas
The content-types and schema the response returns (the schema is created using the Property functions).
 
.PARAMETER HeaderSchemas
The header name and schema the response returns (the schema is created using the Property functions).
 
.PARAMETER Description
The Description of the response.
 
.EXAMPLE
Add-PodeOAComponentResponse -Name 'OKResponse' -ContentSchemas @{ 'application/json' = (New-PodeOAIntProperty -Name 'userId' -Object) }
 
.EXAMPLE
Add-PodeOAComponentResponse -Name 'ErrorResponse' -ContentSchemas @{ 'application/json' = 'ErrorSchema' }
#>

function Add-PodeOAComponentResponse
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter()]
        [hashtable]
        $ContentSchemas,

        [Parameter()]
        [hashtable]
        $HeaderSchemas,

        [Parameter(Mandatory=$true)]
        [string]
        $Description
    )

    $content = $null
    if ($null -ne $ContentSchemas) {
        $content = ($ContentSchemas | ConvertTo-PodeOAContentTypeSchema)
    }

    $headers = $null
    if ($null -ne $HeaderSchemas) {
        $headers = ($HeaderSchemas | ConvertTo-PodeOAHeaderSchema)
    }

    $PodeContext.Server.OpenAPI.components.responses[$Name] = @{
        description = $Description
        content = $content
        headers = $headers
    }
}

<#
.SYNOPSIS
Sets the names of defined Authentication types as the security the supplied route uses.
 
.DESCRIPTION
Sets the names of defined Authentication types as the security the supplied route uses.
 
.PARAMETER Route
The route to set a security definition, usually from -PassThru on Add-PodeRoute.
 
.PARAMETER Name
The Name(s) of any defined Authentication types (from Add-PodeAuth).
 
.PARAMETER PassThru
If supplied, the route passed in will be returned for further chaining.
 
.EXAMPLE
Add-PodeRoute -PassThru | Set-PodeOAAuth -Name 'Validate'
#>

function Set-PodeOAAuth
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [ValidateNotNullOrEmpty()]
        [hashtable[]]
        $Route,

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

        [switch]
        $PassThru
    )

    foreach ($n in @($Name)) {
        if (!$PodeContext.Server.Authentications.ContainsKey($n)) {
            throw "Authentication method does not exist: $($n)"
        }
    }

    foreach ($r in @($Route)) {
        $r.OpenApi.Authentication = @(foreach ($n in @($Name)) {
            @{
                "$($n -replace '\s+', '')" = @()
            }
        })
    }

    if ($PassThru) {
        return $Route
    }
}

<#
.SYNOPSIS
Sets the names of defined Authentication types as global OpenAPI Security.
 
.DESCRIPTION
Sets the names of defined Authentication types as global OpenAPI Security.
 
.PARAMETER Name
The Name(s) of any defined Authentication types (from Add-PodeAuth).
 
.EXAMPLE
Set-PodeOAGlobalAuth -Name 'Validate'
#>

function Set-PodeOAGlobalAuth
{
    [CmdletBinding()]
    param(
        [Parameter()]
        [string[]]
        $Name
    )

    foreach ($n in @($Name)) {
        if (!$PodeContext.Server.Authentications.ContainsKey($n)) {
            throw "Authentication method does not exist: $($n)"
        }
    }

    $PodeContext.Server.OpenAPI.security = @(foreach ($n in @($Name)) {
        @{
            "$($n -replace '\s+', '')" = @()
        }
    })
}

<#
.SYNOPSIS
Sets the definition of a request for a route.
 
.DESCRIPTION
Sets the definition of a request for a route.
 
.PARAMETER Route
The route to set a request definition, usually from -PassThru on Add-PodeRoute.
 
.PARAMETER Parameters
The Parameter definitions the request uses (from ConvertTo-PodeOAParameter).
 
.PARAMETER RequestBody
The Request Body definition the request uses (from New-PodeOARequestBody).
 
.PARAMETER PassThru
If supplied, the route passed in will be returned for further chaining.
 
.EXAMPLE
Add-PodeRoute -PassThru | Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Reference 'UserIdBody')
#>

function Set-PodeOARequest
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [ValidateNotNullOrEmpty()]
        [hashtable[]]
        $Route,

        [Parameter()]
        [hashtable[]]
        $Parameters,

        [Parameter()]
        [hashtable]
        $RequestBody,

        [switch]
        $PassThru
    )

    foreach ($r in @($Route)) {
        if (($null -ne $Parameters) -and ($Parameters.Length -gt 0)) {
            $r.OpenApi.Parameters = @($Parameters)
        }

        if ($null -ne $RequestBody) {
            $r.OpenApi.RequestBody = $RequestBody
        }
    }

    if ($PassThru) {
        return $Route
    }
}

<#
.SYNOPSIS
Creates a Request Body definition for routes.
 
.DESCRIPTION
Creates a Request Body definition for routes from the supplied content-types and schemas.
 
.PARAMETER Reference
A reference name from an existing component request body.
 
.PARAMETER ContentSchemas
The content-types and schema the request body accepts (the schema is created using the Property functions).
 
.PARAMETER Description
A Description of the request body.
 
.PARAMETER Required
If supplied, the request body will be flagged as required.
 
.EXAMPLE
New-PodeOARequestBody -ContentSchemas @{ 'application/json' = (New-PodeOAIntProperty -Name 'userId' -Object) }
 
.EXAMPLE
New-PodeOARequestBody -ContentSchemas @{ 'application/json' = 'UserIdSchema' }
 
.EXAMPLE
New-PodeOARequestBody -Reference 'UserIdBody'
#>

function New-PodeOARequestBody
{
    [CmdletBinding(DefaultParameterSetName='Schema')]
    param(
        [Parameter(Mandatory=$true, ParameterSetName='Reference')]
        [string]
        $Reference,

        [Parameter(Mandatory=$true, ParameterSetName='Schema')]
        [hashtable]
        $ContentSchemas,

        [Parameter(ParameterSetName='Schema')]
        [string]
        $Description = $null,

        [Parameter(ParameterSetName='Schema')]
        [switch]
        $Required
    )

    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'schema' {
            return @{
                required = $Required.IsPresent
                description = $Description
                content = ($ContentSchemas | ConvertTo-PodeOAContentTypeSchema)
            }
        }

        'reference' {
            if (!(Test-PodeOAComponentRequestBody -Name $Reference)) {
                throw "The OpenApi component request body doesn't exist: $($Reference)"
            }

            return = @{
                '$ref' = "#/components/requestBodies/$($Reference)"
            }
        }
    }
}

<#
.SYNOPSIS
Adds a reusable component for a request body.
 
.DESCRIPTION
Adds a reusable component for a request body.
 
.PARAMETER Name
The reference Name of the schema.
 
.PARAMETER Schema
The Schema definition (the schema is created using the Property functions).
 
.EXAMPLE
Add-PodeOAComponentSchema -Name 'UserIdSchema' -Schema (New-PodeOAIntProperty -Name 'userId' -Object)
#>

function Add-PodeOAComponentSchema
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [hashtable]
        $Schema
    )

    $PodeContext.Server.OpenAPI.components.schemas[$Name] = ($Schema | ConvertTo-PodeOASchemaProperty)
}

<#
.SYNOPSIS
Adds a reusable component for a request body.
 
.DESCRIPTION
Adds a reusable component for a request body.
 
.PARAMETER Name
The reference Name of the request body.
 
.PARAMETER ContentSchemas
The content-types and schema the request body accepts (the schema is created using the Property functions).
 
.PARAMETER Description
A Description of the request body.
 
.PARAMETER Required
If supplied, the request body will be flagged as required.
 
.EXAMPLE
Add-PodeOAComponentRequestBody -Name 'UserIdBody' -ContentSchemas @{ 'application/json' = (New-PodeOAIntProperty -Name 'userId' -Object) }
 
.EXAMPLE
Add-PodeOAComponentRequestBody -Name 'UserIdBody' -ContentSchemas @{ 'application/json' = 'UserIdSchema' }
#>

function Add-PodeOAComponentRequestBody
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [hashtable]
        $ContentSchemas,

        [Parameter()]
        [string]
        $Description = $null,

        [Parameter()]
        [switch]
        $Required
    )

    $PodeContext.Server.OpenAPI.components.requestBodies[$Name] = @{
        required = $Required.IsPresent
        description = $Description
        content = ($ContentSchemas | ConvertTo-PodeOAContentTypeSchema)
    }
}

<#
.SYNOPSIS
Adds a reusable component for a request parameter.
 
.DESCRIPTION
Adds a reusable component for a request parameter.
 
.PARAMETER Name
The reference Name of the parameter.
 
.PARAMETER Parameter
The Parameter to use for the component (from ConvertTo-PodeOAParameter)
 
.EXAMPLE
New-PodeOAIntProperty -Name 'userId' | ConvertTo-PodeOAParameter -In Query | Add-PodeOAComponentParameter -Name 'UserIdParam'
#>

function Add-PodeOAComponentParameter
{
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name,

        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [hashtable]
        $Parameter
    )

    if ([string]::IsNullOrWhiteSpace($Name)) {
        $Name = $Parameter.name
    }

    $PodeContext.Server.OpenAPI.components.responses[$Name] = $Parameter
}

<#
.SYNOPSIS
Creates a new OpenAPI integer property.
 
.DESCRIPTION
Creates a new OpenAPI integer property, for Schemas or Parameters.
 
.PARAMETER Name
The Name of the property.
 
.PARAMETER Format
The inbuilt OpenAPI Format of the integer. (Default: Any)
 
.PARAMETER Default
The default value of the property. (Default: 0)
 
.PARAMETER Minimum
The minimum value of the integer. (Default: Int.Min)
 
.PARAMETER Maximum
The maximum value of the integer. (Default: Int.Max)
 
.PARAMETER MultiplesOf
The integer must be in multiples of the supplied value.
 
.PARAMETER Description
A Description of the property.
 
.PARAMETER Required
If supplied, the object will be treated as Required where supported.
 
.PARAMETER Deprecated
If supplied, the object will be treated as Deprecated where supported.
 
.PARAMETER Array
If supplied, the integer will be treated as an array of integers.
 
.PARAMETER Object
If supplied, the integer will be automatically wrapped in an object.
 
.EXAMPLE
New-PodeOANumberProperty -Name 'age' -Required
#>

function New-PodeOAIntProperty
{
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [ValidateSet('', 'Int32', 'Int64')]
        [string]
        $Format,

        [Parameter()]
        [int]
        $Default = 0,

        [Parameter()]
        [int]
        $Minimum = [int]::MinValue,

        [Parameter()]
        [int]
        $Maximum = [int]::MaxValue,

        [Parameter()]
        [int]
        $MultiplesOf = 0,

        [Parameter()]
        [string]
        $Description,

        [switch]
        $Required,

        [switch]
        $Deprecated,

        [switch]
        $Array,

        [switch]
        $Object
    )

    $param = @{
        name = $Name
        type = 'integer'
        array = $Array.IsPresent
        object = $Object.IsPresent
        required = $Required.IsPresent
        deprecated = $Deprecated.IsPresent
        description = $Description
        format = $Format.ToLowerInvariant()
        default = $Default
    }

    if ($Minimum -ne [int]::MinValue) {
        $param['minimum'] = $Minimum
    }

    if ($Maximum -ne [int]::MaxValue) {
        $param['maximum'] = $Maximum
    }

    if ($MultiplesOf -ne 0) {
        $param['multipleOf'] = $MultiplesOf
    }

    return $param
}

<#
.SYNOPSIS
Creates a new OpenAPI number property.
 
.DESCRIPTION
Creates a new OpenAPI number property, for Schemas or Parameters.
 
.PARAMETER Name
The Name of the property.
 
.PARAMETER Format
The inbuilt OpenAPI Format of the number. (Default: Any)
 
.PARAMETER Default
The default value of the property. (Default: 0)
 
.PARAMETER Minimum
The minimum value of the number. (Default: Double.Min)
 
.PARAMETER Maximum
The maximum value of the number. (Default: Double.Max)
 
.PARAMETER MultiplesOf
The number must be in multiples of the supplied value.
 
.PARAMETER Description
A Description of the property.
 
.PARAMETER Required
If supplied, the object will be treated as Required where supported.
 
.PARAMETER Deprecated
If supplied, the object will be treated as Deprecated where supported.
 
.PARAMETER Array
If supplied, the number will be treated as an array of numbers.
 
.PARAMETER Object
If supplied, the number will be automatically wrapped in an object.
 
.EXAMPLE
New-PodeOANumberProperty -Name 'gravity' -Default 9.8
#>

function New-PodeOANumberProperty
{
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [ValidateSet('', 'Double', 'Float')]
        [string]
        $Format,

        [Parameter()]
        [double]
        $Default = 0,

        [Parameter()]
        [double]
        $Minimum = [double]::MinValue,

        [Parameter()]
        [double]
        $Maximum = [double]::MaxValue,

        [Parameter()]
        [double]
        $MultiplesOf = 0,

        [Parameter()]
        [string]
        $Description,

        [switch]
        $Required,

        [switch]
        $Deprecated,

        [switch]
        $Array,

        [switch]
        $Object
    )

    $param = @{
        name = $Name
        type = 'number'
        array = $Array.IsPresent
        object = $Object.IsPresent
        required = $Required.IsPresent
        deprecated = $Deprecated.IsPresent
        description = $Description
        format = $Format.ToLowerInvariant()
        default = $Default
    }

    if ($Minimum -ne [double]::MinValue) {
        $param['minimum'] = $Minimum
    }

    if ($Maximum -ne [double]::MaxValue) {
        $param['maximum'] = $Maximum
    }

    if ($MultiplesOf -ne 0) {
        $param['multipleOf'] = $MultiplesOf
    }

    return $param
}

<#
.SYNOPSIS
Creates a new OpenAPI string property.
 
.DESCRIPTION
Creates a new OpenAPI string property, for Schemas or Parameters.
 
.PARAMETER Name
The Name of the property.
 
.PARAMETER Format
The inbuilt OpenAPI Format of the string. (Default: Any)
 
.PARAMETER CustomFormat
The name of a custom OpenAPI Format of the string. (Default: None)
 
.PARAMETER Default
The default value of the property. (Default: $null)
 
.PARAMETER MinLength
The minimum length of the string. (Default: Int.Min)
 
.PARAMETER MaxLength
The maximum length of the string. (Default: Int.Max)
 
.PARAMETER Pattern
A Regex pattern that the string must match.
 
.PARAMETER Description
A Description of the property.
 
.PARAMETER Required
If supplied, the object will be treated as Required where supported.
 
.PARAMETER Deprecated
If supplied, the object will be treated as Deprecated where supported.
 
.PARAMETER Array
If supplied, the string will be treated as an array of strings.
 
.PARAMETER Object
If supplied, the string will be automatically wrapped in an object.
 
.EXAMPLE
New-PodeOAStringProperty -Name 'userType' -Default 'admin'
 
.EXAMPLE
New-PodeOAStringProperty -Name 'password' -Format Password
#>

function New-PodeOAStringProperty
{
    [CmdletBinding(DefaultParameterSetName='Inbuilt')]
    param(
        [Parameter()]
        [string]
        $Name,

        [Parameter(ParameterSetName='Inbuilt')]
        [ValidateSet('', 'Binary', 'Byte', 'Date', 'Date-Time', 'Password')]
        [string]
        $Format,

        [Parameter(ParameterSetName='Custom')]
        [string]
        $CustomFormat,

        [Parameter()]
        [string]
        $Default = $null,

        [Parameter()]
        [int]
        $MinLength = [int]::MinValue,

        [Parameter()]
        [int]
        $MaxLength = [int]::MaxValue,

        [Parameter()]
        [string]
        $Pattern = $null,

        [Parameter()]
        [string]
        $Description,

        [switch]
        $Required,

        [switch]
        $Deprecated,

        [switch]
        $Array,

        [switch]
        $Object
    )

    $_format = $Format
    if (![string]::IsNullOrWhiteSpace($CustomFormat)) {
        $_format = $CustomFormat
    }

    $param = @{
        name = $Name
        type = 'string'
        array = $Array.IsPresent
        object = $Object.IsPresent
        required = $Required.IsPresent
        deprecated = $Deprecated.IsPresent
        description = $Description
        format = $_format.ToLowerInvariant()
        pattern = $Pattern
        default = $Default
    }

    if ($MinLength -ne [int]::MinValue) {
        $param['minLength'] = $MinLength
    }

    if ($MaxLength -ne [int]::MaxValue) {
        $param['maxLength'] = $MaxLength
    }

    return $param
}

<#
.SYNOPSIS
Creates a new OpenAPI boolean property.
 
.DESCRIPTION
Creates a new OpenAPI boolean property, for Schemas or Parameters.
 
.PARAMETER Name
The Name of the property.
 
.PARAMETER Default
The default value of the property. (Default: $false)
 
.PARAMETER Description
A Description of the property.
 
.PARAMETER Required
If supplied, the object will be treated as Required where supported.
 
.PARAMETER Deprecated
If supplied, the object will be treated as Deprecated where supported.
 
.PARAMETER Array
If supplied, the boolean will be treated as an array of booleans.
 
.PARAMETER Object
If supplied, the boolean will be automatically wrapped in an object.
 
.EXAMPLE
New-PodeOABoolProperty -Name 'enabled' -Required
#>

function New-PodeOABoolProperty
{
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [bool]
        $Default = $false,

        [Parameter()]
        [string]
        $Description,

        [switch]
        $Required,

        [switch]
        $Deprecated,

        [switch]
        $Array,

        [switch]
        $Object
    )

    $param = @{
        name = $Name
        type = 'boolean'
        array = $Array.IsPresent
        object = $Object.IsPresent
        required = $Required.IsPresent
        deprecated = $Deprecated.IsPresent
        description = $Description
        default = $Default
    }

    return $param
}

<#
.SYNOPSIS
Creates a new OpenAPI object property from other properties.
 
.DESCRIPTION
Creates a new OpenAPI object property from other properties, for Schemas or Parameters.
 
.PARAMETER Name
The Name of the property.
 
.PARAMETER Properties
An array of other int/string/etc properties wrap up as an object.
 
.PARAMETER Description
A Description of the property.
 
.PARAMETER Required
If supplied, the object will be treated as Required where supported.
 
.PARAMETER Deprecated
If supplied, the object will be treated as Deprecated where supported.
 
.PARAMETER Array
If supplied, the object will be treated as an array of objects.
 
.EXAMPLE
New-PodeOAObjectProperty -Name 'user' -Properties @('<ARRAY_OF_PROPERTIES>')
#>

function New-PodeOAObjectProperty
{
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name,

        [Parameter(Mandatory=$true)]
        [hashtable[]]
        $Properties,

        [Parameter()]
        [string]
        $Description,

        [switch]
        $Required,

        [switch]
        $Deprecated,

        [switch]
        $Array
    )

    $param = @{
        name = $Name
        type = 'object'
        array = $Array.IsPresent
        required = $Required.IsPresent
        deprecated = $Deprecated.IsPresent
        description = $Description
        properties = $Properties
        default = $Default
    }

    return $param
}

<#
.SYNOPSIS
Converts an OpenAPI property into a Request Parameter.
 
.DESCRIPTION
Converts an OpenAPI property (such as from New-PodeOAIntProperty) into a Request Parameter.
 
.PARAMETER In
Where in the Request can the parameter be found?
 
.PARAMETER Property
The Property that need converting (such as from New-PodeOAIntProperty).
 
.PARAMETER Reference
The name of an existing component parameter to be reused.
 
.EXAMPLE
New-PodeOAIntProperty -Name 'userId' | ConvertTo-PodeOAParameter -In Query
 
.EXAMPLE
ConvertTo-PodeOAParameter -Reference 'UserIdParam'
#>

function ConvertTo-PodeOAParameter
{
    [CmdletBinding(DefaultParameterSetName='Reference')]
    param(
        [Parameter(Mandatory=$true, ParameterSetName='Schema')]
        [ValidateSet('Cookie', 'Header', 'Path', 'Query')]
        [string]
        $In,

        [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName='Schema')]
        [ValidateNotNull()]
        [hashtable]
        $Property,

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

    # return a reference
    if ($PSCmdlet.ParameterSetName -ieq 'reference') {
        if (!(Test-PodeOAComponentParameter -Name $Reference)) {
            throw "The OpenApi component request parameter doesn't exist: $($Reference)"
        }

        return = @{
            '$ref' = "#/components/parameters/$($Reference)"
        }
    }

    # non-object/array only
    if (@('array', 'object') -icontains $Property.type) {
        throw "OpenApi request parameter cannot be an array of object"
    }

    # build the base parameter
    $prop = @{
        in = $In.ToLowerInvariant()
        name = $Property.name
        required = $Property.required
        description = $Property.description
        deprecated = $Property.deprecated
        schema = @{
            type = $Property.type
            format = $Property.format
        }
    }

    # remove default for required parameter
    if (!$Property.required) {
        $prop.schema['default'] = $Property.default
    }

    return $prop
}

<#
.SYNOPSIS
Sets metadate for the supplied route.
 
.DESCRIPTION
Sets metadate for the supplied route, such as Summary and Tags.
 
.PARAMETER Route
The route to update info, usually from -PassThru on Add-PodeRoute.
 
.PARAMETER Summary
A quick Summary of the route.
 
.PARAMETER Description
A longer Description of the route.
 
.PARAMETER Tags
An array of Tags for the route, mostly for grouping.
 
.PARAMETER Deprecated
If supplied, the route will be flagged as deprecated.
 
.PARAMETER PassThru
If supplied, the route passed in will be returned for further chaining.
 
.EXAMPLE
Add-PodeRoute -PassThru | Set-PodeOARouteInfo -Summary 'A quick summary' -Tags 'Admin'
#>

function Set-PodeOARouteInfo
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [ValidateNotNullOrEmpty()]
        [hashtable[]]
        $Route,

        [Parameter()]
        [string]
        $Summary,

        [Parameter()]
        [string]
        $Description,

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

        [switch]
        $Deprecated,

        [switch]
        $PassThru
    )

    foreach ($r in @($Route)) {
        $r.OpenApi.Summary = $Summary
        $r.OpenApi.Description = $Description
        $r.OpenApi.Tags = $Tags
        $r.OpenApi.Deprecated = $Deprecated.IsPresent
    }

    if ($PassThru) {
        return $Route
    }
}

<#
.SYNOPSIS
Adds a route that enables a viewer to display OpenAPI docs, such as Swagger or ReDoc.
 
.DESCRIPTION
Adds a route that enables a viewer to display OpenAPI docs, such as Swagger or ReDoc.
 
.PARAMETER Type
The Type of OpenAPI viewer to use.
 
.PARAMETER Path
The route Path where the docs can be accessed. (Default: "/$Type")
 
.PARAMETER OpenApiUrl
The URL where the OpenAPI definition can be retrieved. (Default is the OpenAPI path from Enable-PodeOpenApi)
 
.PARAMETER Middleware
Like normal Routes, an array of Middleware that will be applied.
 
.PARAMETER Title
The title of the web page.
 
.PARAMETER DarkMode
If supplied, the page will be rendered using a dark theme (this is not supported for all viewers).
 
.EXAMPLE
Enable-PodeOpenApiViewer -Type Swagger -DarkMode
 
.EXAMPLE
Enable-PodeOpenApiViewer -Type ReDoc -Title 'Some Title' -OpenApi 'http://some-url/openapi'
#>

function Enable-PodeOpenApiViewer
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateSet('Swagger', 'ReDoc')]
        [string]
        $Type,

        [Parameter()]
        [string]
        $Path,

        [Parameter()]
        [string]
        $OpenApiUrl,

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

        [Parameter()]
        [string]
        $Title,

        [switch]
        $DarkMode
    )

    # error if there's no OpenAPI URL
    $OpenApiUrl = Protect-PodeValue -Value $OpenApiUrl -Default $PodeContext.Server.OpenAPI.Path
    if ([string]::IsNullOrWhiteSpace($OpenApiUrl)) {
        throw "No OpenAPI URL supplied for $($Type)"
    }

    # fail if no title
    $Title = Protect-PodeValue -Value $Title -Default $PodeContext.Server.OpenAPI.Title
    $Title = Protect-PodeValue -Value $Title -Default $Type
    if ([string]::IsNullOrWhiteSpace($Title)) {
        throw "No title supplied for $($Type) page"
    }

    # set a default path
    $Path = Protect-PodeValue -Value $Path -Default "/$($Type.ToLowerInvariant())"
    if ([string]::IsNullOrWhiteSpace($Title)) {
        throw "No route path supplied for $($Type) page"
    }

    # setup meta info
    $meta = @{
        Type = $Type.ToLowerInvariant()
        Title = $Title
        OpenApi = $OpenApiUrl
        DarkMode = $DarkMode
    }

    # add the viewer route
    Add-PodeRoute -Method Get -Path $Path -Middleware $Middleware -ArgumentList $meta -ScriptBlock {
        param($e, $meta)
        $podeRoot = Get-PodeModuleMiscPath
        Write-PodeFileResponse -Path (Join-Path $podeRoot "default-$($meta.Type).html.pode") -Data @{
            Title = $meta.Title
            OpenApi = $meta.OpenApi
            DarkMode = $meta.DarkMode
        }
    }
}