Public/JsonSchema.ps1

<#
.SYNOPSIS
Creates a Null JSON Schema type definition.
 
.DESCRIPTION
This function creates a JSON Schema type definition for a Null type.
 
.PARAMETER Description
An optional Description.
 
.EXAMPLE
New-PodeJsonSchemaNull
 
.EXAMPLE
New-PodeJsonSchemaNull -Description 'This is a null type'
#>

function New-PodeJsonSchemaNull {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter()]
        [string]
        $Description
    )

    # build the property definition
    $def = @{
        type = 'null'
    }

    if ($PSBoundParameters.ContainsKey('Description') -and ![string]::IsNullOrEmpty($Description)) {
        $def.description = $Description
    }

    return $def
}

<#
.SYNOPSIS
Creates a Boolean JSON Schema type definition.
 
.DESCRIPTION
This function creates a JSON Schema type definition for a Boolean type.
 
.PARAMETER Constant
An optional Constant value.
 
.PARAMETER Description
An optional Description.
 
.EXAMPLE
New-PodeJsonSchemaBoolean
 
.EXAMPLE
New-PodeJsonSchemaBoolean -Constant $true -Description 'Some property that must be true'
#>

function New-PodeJsonSchemaBoolean {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter()]
        [bool]
        $Constant,

        [Parameter()]
        [string]
        $Description
    )

    # build the property definition
    $def = @{
        type = 'boolean'
    }

    if ($PSBoundParameters.ContainsKey('Constant')) {
        $def.const = $Constant
    }

    if ($PSBoundParameters.ContainsKey('Description') -and ![string]::IsNullOrEmpty($Description)) {
        $def.description = $Description
    }

    return $def
}

<#
.SYNOPSIS
Creates an Integer JSON Schema type definition.
 
.DESCRIPTION
This function creates a JSON Schema type definition for an Integer type.
 
.PARAMETER MultipleOf
An optional value that the integer must be a multiple of.
 
.PARAMETER Minimum
An optional minimum value for the integer.
 
.PARAMETER Maximum
An optional maximum value for the integer.
 
.PARAMETER Constant
An optional Constant value.
 
.PARAMETER Enum
An optional array of values that the integer can be.
 
.PARAMETER ExclusiveMinimum
An optional switch to indicate if the minimum value is exclusive.
 
.PARAMETER ExclusiveMaximum
An optional switch to indicate if the maximum value is exclusive.
 
.PARAMETER Description
An optional Description.
 
.EXAMPLE
New-PodeJsonSchemaInteger
 
.EXAMPLE
New-PodeJsonSchemaInteger -Minimum 0 -Maximum 100 -Description 'A percentage of some value'
 
.EXAMPLE
New-PodeJsonSchemaInteger -Enum 1, 2, 4, 8 -Description 'Number of CPU cores to use'
 
.EXAMPLE
New-PodeJsonSchemaInteger -MultipleOf 5 -Description 'A value that must be a multiple of 5'
 
.EXAMPLE
New-PodeJsonSchemaInteger -Minimum 0 -ExclusiveMinimum -Description 'A value that must be greater than 0'
 
.EXAMPLE
New-PodeJsonSchemaInteger -Maximum 100 -ExclusiveMaximum -Description 'A value that must be less than 100'
#>

function New-PodeJsonSchemaInteger {
    [CmdletBinding(DefaultParameterSetName = 'Dynamic')]
    [OutputType([hashtable])]
    param(
        [Parameter(ParameterSetName = 'Dynamic')]
        [ValidateRange(0, [int]::MaxValue)]
        [int]
        $MultipleOf,

        [Parameter(ParameterSetName = 'Dynamic')]
        [int]
        $Minimum,

        [Parameter(ParameterSetName = 'Dynamic')]
        [int]
        $Maximum,

        [Parameter(ParameterSetName = 'Constant')]
        [string]
        $Constant,

        [Parameter(ParameterSetName = 'Enum')]
        [int[]]
        $Enum,

        [Parameter(ParameterSetName = 'Dynamic')]
        [switch]
        $ExclusiveMinimum,

        [Parameter(ParameterSetName = 'Dynamic')]
        [switch]
        $ExclusiveMaximum,

        [Parameter()]
        [string]
        $Description
    )

    # build the property definition
    $def = @{
        type = 'integer'
    }

    if ($PSBoundParameters.ContainsKey('Constant')) {
        $def.const = $Constant
    }

    if ($PSBoundParameters.ContainsKey('Enum')) {
        $def.enum = @($Enum)
    }

    if ($PSBoundParameters.ContainsKey('MultipleOf')) {
        $def.multipleOf = $MultipleOf
    }

    if ($PSBoundParameters.ContainsKey('Minimum')) {
        $def.minimum = $Minimum
    }
    if ($PSBoundParameters.ContainsKey('Maximum')) {
        $def.maximum = $Maximum
    }
    if ($def.ContainsKey('minimum') -and $def.ContainsKey('maximum') -and ($def.maximum -lt $def.minimum)) {
        # Maximum cannot be less than Minimum for integer JSON Schema property'
        throw ($PodeLocale.jsonSchemaNumberMaximumLessThanMinimumExceptionMessage -f 'integer')
    }

    if ($PSBoundParameters.ContainsKey('ExclusiveMinimum')) {
        $def.exclusiveMinimum = $true
    }
    if ($PSBoundParameters.ContainsKey('ExclusiveMaximum')) {
        $def.exclusiveMaximum = $true
    }

    if ($PSBoundParameters.ContainsKey('Description') -and ![string]::IsNullOrEmpty($Description)) {
        $def.description = $Description
    }

    return $def
}

<#
.SYNOPSIS
Creates a Number JSON Schema type definition.
 
.DESCRIPTION
This function creates a JSON Schema type definition for a Number type.
 
.PARAMETER MultipleOf
An optional value that the number must be a multiple of.
 
.PARAMETER Minimum
An optional minimum value for the number.
 
.PARAMETER Maximum
An optional maximum value for the number.
 
.PARAMETER Constant
An optional Constant value.
 
.PARAMETER Enum
An optional array of values that the number can be.
 
.PARAMETER ExclusiveMinimum
An optional switch to indicate if the minimum value is exclusive.
 
.PARAMETER ExclusiveMaximum
An optional switch to indicate if the maximum value is exclusive.
 
.PARAMETER Description
An optional Description.
 
.EXAMPLE
New-PodeJsonSchemaNumber
 
.EXAMPLE
New-PodeJsonSchemaNumber -Minimum 0.0 -Maximum 1.0 -Description 'A ratio between 0 and 1'
 
.EXAMPLE
New-PodeJsonSchemaNumber -Enum 3.14, 2.718, 1.618 -Description 'Some famous mathematical constants'
 
.EXAMPLE
New-PodeJsonSchemaNumber -MultipleOf 0.01 -Description 'A value that must be a multiple of 0.01'
 
.EXAMPLE
New-PodeJsonSchemaNumber -Minimum 0.0 -ExclusiveMinimum -Description 'A value that must be greater than 0.0'
 
.EXAMPLE
New-PodeJsonSchemaNumber -Maximum 100.0 -ExclusiveMaximum -Description 'A value that must be less than 100.0'
#>

function New-PodeJsonSchemaNumber {
    [CmdletBinding(DefaultParameterSetName = 'Dynamic')]
    [OutputType([hashtable])]
    param(
        [Parameter(ParameterSetName = 'Dynamic')]
        [ValidateRange(0, [int]::MaxValue)]
        [double]
        $MultipleOf,

        [Parameter(ParameterSetName = 'Dynamic')]
        [double]
        $Minimum,

        [Parameter(ParameterSetName = 'Dynamic')]
        [double]
        $Maximum,

        [Parameter(ParameterSetName = 'Constant')]
        [string]
        $Constant,

        [Parameter(ParameterSetName = 'Enum')]
        [double[]]
        $Enum,

        [Parameter(ParameterSetName = 'Dynamic')]
        [switch]
        $ExclusiveMinimum,

        [Parameter(ParameterSetName = 'Dynamic')]
        [switch]
        $ExclusiveMaximum,

        [Parameter()]
        [string]
        $Description
    )

    # build the property definition
    $def = @{
        type = 'number'
    }

    if ($PSBoundParameters.ContainsKey('Constant')) {
        $def.const = $Constant
    }

    if ($PSBoundParameters.ContainsKey('Enum')) {
        $def.enum = @($Enum)
    }

    if ($PSBoundParameters.ContainsKey('MultipleOf')) {
        $def.multipleOf = $MultipleOf
    }

    if ($PSBoundParameters.ContainsKey('Minimum')) {
        $def.minimum = $Minimum
    }
    if ($PSBoundParameters.ContainsKey('Maximum')) {
        $def.maximum = $Maximum
    }
    if ($def.ContainsKey('minimum') -and $def.ContainsKey('maximum') -and ($def.maximum -lt $def.minimum)) {
        # Maximum cannot be less than Minimum for number JSON Schema property
        throw ($PodeLocale.jsonSchemaNumberMaximumLessThanMinimumExceptionMessage -f 'number')
    }

    if ($PSBoundParameters.ContainsKey('ExclusiveMinimum')) {
        $def.exclusiveMinimum = $true
    }
    if ($PSBoundParameters.ContainsKey('ExclusiveMaximum')) {
        $def.exclusiveMaximum = $true
    }

    if ($PSBoundParameters.ContainsKey('Description') -and ![string]::IsNullOrEmpty($Description)) {
        $def.description = $Description
    }

    return $def
}

<#
.SYNOPSIS
Creates a String JSON Schema type definition.
 
.DESCRIPTION
This function creates a JSON Schema type definition for a String type.
 
.PARAMETER Pattern
An optional regular expression pattern that the string must match.
 
.PARAMETER MinLength
An optional minimum length for the string.
 
.PARAMETER MaxLength
An optional maximum length for the string.
 
.PARAMETER Constant
An optional Constant value.
 
.PARAMETER Enum
An optional array of values that the string can be.
 
.PARAMETER Description
An optional Description.
 
.EXAMPLE
New-PodeJsonSchemaString
 
.EXAMPLE
New-PodeJsonSchemaString -Pattern '^[a-zA-Z0-9]+$' -Description 'A string that must be alphanumeric'
 
.EXAMPLE
New-PodeJsonSchemaString -MinLength 5 -MaxLength 10 -Description 'A string that must be between 5 and 10 characters long'
 
.EXAMPLE
New-PodeJsonSchemaString -Enum 'red', 'green', 'blue' -Description 'A string that must be one of the specified colours'
 
.EXAMPLE
New-PodeJsonSchemaString -Constant 'fixed value' -Description 'A string that must be exactly "fixed value"'
#>

function New-PodeJsonSchemaString {
    [CmdletBinding(DefaultParameterSetName = 'Dynamic')]
    [OutputType([hashtable])]
    param(
        [Parameter(ParameterSetName = 'Dynamic')]
        [string]
        $Pattern,

        [Parameter(ParameterSetName = 'Dynamic')]
        [ValidateRange(0, [int]::MaxValue)]
        [int]
        $MinLength,

        [Parameter(ParameterSetName = 'Dynamic')]
        [ValidateRange(0, [int]::MaxValue)]
        [int]
        $MaxLength,

        [Parameter(ParameterSetName = 'Constant')]
        [string]
        $Constant,

        [Parameter(ParameterSetName = 'Enum')]
        [string[]]
        $Enum,

        [Parameter()]
        [string]
        $Description
    )

    # build the property definition
    $def = @{
        type = 'string'
    }

    if ($PSBoundParameters.ContainsKey('Constant')) {
        $def.const = $Constant
    }

    if ($PSBoundParameters.ContainsKey('Enum')) {
        $def.enum = @($Enum)
    }

    if ($PSBoundParameters.ContainsKey('Pattern')) {
        $def.pattern = $Pattern
    }

    if ($PSBoundParameters.ContainsKey('MinLength')) {
        $def.minLength = $MinLength
    }
    if ($PSBoundParameters.ContainsKey('MaxLength')) {
        $def.maxLength = $MaxLength
    }
    if ($def.ContainsKey('minLength') -and $def.ContainsKey('maxLength') -and ($def.maxLength -lt $def.minLength)) {
        # MaxLength cannot be less than MinLength for string JSON Schema property
        throw $PodeLocale.jsonSchemaStringMaxLengthLessThanMinLengthExceptionMessage
    }

    if ($PSBoundParameters.ContainsKey('Description') -and ![string]::IsNullOrEmpty($Description)) {
        $def.description = $Description
    }

    return $def
}

<#
.SYNOPSIS
Creates an Array JSON Schema type definition.
 
.DESCRIPTION
This function creates a JSON Schema type definition for an Array type.
 
.PARAMETER Item
A hashtable representing the JSON Schema type definition for the items in the array.
 
.PARAMETER MinItems
An optional minimum number of items in the array.
 
.PARAMETER MaxItems
An optional maximum number of items in the array.
 
.PARAMETER Description
An optional Description.
 
.PARAMETER Unique
An optional switch to indicate if the items in the array must be unique.
 
.EXAMPLE
# create a JSON Schema definition for an array of strings
New-PodeJsonSchemaArray -Item (New-PodeJsonSchemaString)
 
.EXAMPLE
# create a JSON Schema definition for an array of integers with at least 1 item and at most 5 items
New-PodeJsonSchemaArray -Item (New-PodeJsonSchemaInteger) -MinItems 1 -MaxItems 5
 
.EXAMPLE
# create a JSON Schema definition for an array of unique strings with a description
New-PodeJsonSchemaArray -Item (New-PodeJsonSchemaString) -Unique
#>

function New-PodeJsonSchemaArray {
    [CmdletBinding(DefaultParameterSetName = 'Items')]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'Items')]
        [hashtable]
        $Item,

        [Parameter(ParameterSetName = 'Items')]
        [ValidateRange(0, [int]::MaxValue)]
        [int]
        $MinItems,

        [Parameter(ParameterSetName = 'Items')]
        [ValidateRange(0, [int]::MaxValue)]
        [int]
        $MaxItems,

        [Parameter()]
        [string]
        $Description,

        [switch]
        $Unique
    )

    # build the property definition
    $def = @{
        type  = 'array'
        items = $Item
    }

    if ($PSBoundParameters.ContainsKey('MinItems')) {
        $def.minItems = $MinItems
    }
    if ($PSBoundParameters.ContainsKey('MaxItems')) {
        $def.maxItems = $MaxItems
    }
    if ($def.ContainsKey('minItems') -and $def.ContainsKey('maxItems') -and ($def.maxItems -lt $def.minItems)) {
        # MaxItems cannot be less than MinItems for array JSON Schema property
        throw $PodeLocale.jsonSchemaArrayMaxItemsLessThanMinItemsExceptionMessage
    }

    if ($PSBoundParameters.ContainsKey('Unique')) {
        $def.uniqueItems = $Unique.IsPresent
    }

    if ($PSBoundParameters.ContainsKey('Description') -and ![string]::IsNullOrEmpty($Description)) {
        $def.description = $Description
    }

    return $def
}

<#
.SYNOPSIS
Creates an Object JSON Schema type definition.
 
.DESCRIPTION
This function creates a JSON Schema type definition for an Object type.
 
.PARAMETER Property
A hashtable representing the JSON Schema property definition for a property of the object.
This parameter can be specified multiple times to define multiple properties.
Property definitions should be created using the New-PodeJsonSchemaProperty function.
 
.PARAMETER MinProperties
An optional minimum number of properties in the object.
 
.PARAMETER MaxProperties
An optional maximum number of properties in the object.
 
.PARAMETER Description
An optional description.
 
.EXAMPLE
# create a JSON Schema definition for an object with a required string property "name" and an optional integer property "age"
New-PodeJsonSchemaObject -Property @(
    (New-PodeJsonSchemaProperty -Name 'name' -Definition (New-PodeJsonSchemaString) -Required),
    (New-PodeJsonSchemaProperty -Name 'age' -Definition (New-PodeJsonSchemaInteger))
) -Description 'A person object with a required name and an optional age'
 
.EXAMPLE
# create a JSON Schema definition for an object with no properties, but a description - can accept any number of properties
New-PodeJsonSchemaObject -Description 'An empty object with a description'
 
.EXAMPLE
# create a JSON Schema definition for an object with a minimum of 1 property and a maximum of 5 properties
New-PodeJsonSchemaObject -MinProperties 1 -MaxProperties 5
#>

function New-PodeJsonSchemaObject {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter()]
        [hashtable[]]
        $Property,

        [Parameter()]
        [ValidateRange(0, [int]::MaxValue)]
        [int]
        $MinProperties,

        [Parameter()]
        [ValidateRange(0, [int]::MaxValue)]
        [int]
        $MaxProperties,

        [Parameter()]
        [string]
        $Description
    )

    # build the property definition
    $def = @{
        type = 'object'
    }

    if ($PSBoundParameters.ContainsKey('Description') -and ![string]::IsNullOrEmpty($Description)) {
        $def.description = $Description
    }

    if ($PSBoundParameters.ContainsKey('MinProperties')) {
        $def.minProperties = $MinProperties
    }
    if ($PSBoundParameters.ContainsKey('MaxProperties')) {
        $def.maxProperties = $MaxProperties
    }
    if ($def.ContainsKey('minProperties') -and $def.ContainsKey('maxProperties') -and ($def.maxProperties -lt $def.minProperties)) {
        # MaxProperties cannot be less than MinProperties for object JSON Schema property
        throw $PodeLocale.jsonSchemaObjectMaxPropsLessThanMinPropsExceptionMessage
    }

    # if no properties, just return the definition
    if (($null -eq $Property) -or ($Property.Count -eq 0)) {
        return $def
    }

    # otherwise, add the properties to the definition
    $def.properties = @{}
    $requiredProps = @()

    foreach ($prop in $Property) {
        if (!$prop.ContainsKey('Name')) {
            # Each JSON Schema Object property definition must include a "Name" key
            throw $PodeLocale.jsonSchemaObjectPropertyMissingNameExceptionMessage
        }

        $name = $prop.Name
        $def.properties[$name] = $prop.Definition

        if ($prop.Required) {
            $requiredProps += $name
        }
    }

    if ($requiredProps.Length -gt 0) {
        $def.required = $requiredProps
    }

    return $def
}

<#
.SYNOPSIS
Creates a merged JSON Schema type definition using a specified merge type.
 
.DESCRIPTION
This function creates a merged JSON Schema type definition using a specified merge type (AllOf, AnyOf, OneOf, Not)
and an array of JSON Schema definitions to merge.
 
.PARAMETER Type
The type of merge to perform. Must be one of 'AllOf', 'AnyOf', 'OneOf', or 'Not'.
 
.PARAMETER Definition
An array of JSON Schema type definitions to merge.
 
.EXAMPLE
# create a JSON Schema definition that requires a value to match all of the specified schemas
Merge-PodeJsonSchema -Type 'AllOf' -Definition @(
    (New-PodeJsonSchemaString -Pattern '^[a-zA-Z]+$' -Description 'Must be a string of letters only'),
    (New-PodeJsonSchemaString -MinLength 5 -MaxLength 10 -Description 'Must be between 5 and 10 characters long')
)
 
.EXAMPLE
# create a JSON Schema definition that requires a value to match at least one of the specified schemas
Merge-PodeJsonSchema -Type 'AnyOf' -Definition @(
    (New-PodeJsonSchemaInteger -Minimum 0 -Maximum 100 -Description 'A percentage of some value'),
    (New-PodeJsonSchemaString -Enum 'red', 'green', 'blue' -Description 'A string that must be one of the specified colours')
)
 
.EXAMPLE
# create a JSON Schema definition that requires a value to match exactly one of the specified schemas
Merge-PodeJsonSchema -Type 'OneOf' -Definition @(
    (New-PodeJsonSchemaInteger -Minimum 0 -Maximum 100 -Description 'A percentage of some value'),
    (New-PodeJsonSchemaString -Enum 'red', 'green', 'blue' -Description 'A string that must be one of the specified colours')
)
 
.EXAMPLE
# create a JSON Schema definition that requires a value to NOT match any of the specified schemas
Merge-PodeJsonSchema -Type 'Not' -Definition @(
    (New-PodeJsonSchemaInteger -Minimum 0 -Maximum 100 -Description 'A percentage of some value'),
    (New-PodeJsonSchemaString -Enum 'red', 'green', 'blue' -Description 'A string that must be one of the specified colours')
)
#>

function Merge-PodeJsonSchema {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('AllOf', 'AnyOf', 'OneOf', 'Not')]
        [string]
        $Type,

        [Parameter(Mandatory = $true)]
        [hashtable[]]
        $Definition
    )

    $typeName = [string]::Empty
    switch ($Type.ToLowerInvariant()) {
        'allof' { $typeName = 'allOf' }
        'anyof' { $typeName = 'anyOf' }
        'oneof' { $typeName = 'oneOf' }
        'not' { $typeName = 'not' }
    }

    $def = @{
        $typeName = @($Definition)
    }

    return $def
}

<#
.SYNOPSIS
Creates a JSON Schema property definition for use in an object schema.
 
.DESCRIPTION
This function creates a JSON Schema property definition for use in an object schema.
 
.PARAMETER Name
The name of the property.
 
.PARAMETER Definition
A hashtable representing the JSON Schema type definition for the property.
This should be created using one of the other New-PodeJsonSchema* functions.
 
.PARAMETER Required
Indicates whether the property is required.
 
.LINK
https://json-schema.org/understanding-json-schema/reference/type
 
.EXAMPLE
# create a JSON Schema property definition for a required string property "name" with a description
New-PodeJsonSchemaProperty -Name 'name' -Definition (
    New-PodeJsonSchemaString -Description 'The name of the person'
) -Required
 
.EXAMPLE
# create a JSON Schema property definition for an optional integer property "age" with a description
New-PodeJsonSchemaProperty -Name 'age' -Definition (
    New-PodeJsonSchemaInteger -Description 'The age of the person'
)
 
.EXAMPLE
# create a JSON Schema property definition for a required array property "tags" with a description, where the items in the array must be unique strings
New-PodeJsonSchemaProperty -Name 'tags' -Definition (
    New-PodeJsonSchemaArray -Unique -Item (
        New-PodeJsonSchemaString
    ) -Description 'An array of unique tags for the person'
) -Required
 
.EXAMPLE
# create a JSON Schema property definition for an optional property "metadata" with a description, where the value can be any object with a minimum of 1 property
New-PodeJsonSchemaProperty -Name 'metadata' -Definition (
    New-PodeJsonSchemaObject -MinProperties 1 -Description 'Additional metadata about the person'
)
#>

function New-PodeJsonSchemaProperty {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

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

        [switch]
        $Required
    )

    return @{
        Name       = $Name
        Definition = $Definition
        Required   = $Required.IsPresent
    }
}