Private/Generator/ConvertTo-OpenApiGenFunction.ps1

function ConvertTo-OpenApiGenFunction {
    <#
    .SYNOPSIS
        Renders the wrapper function for one operation from the Function.ps1 template.

    .DESCRIPTION
        Takes the resolved command name, the operation, its parameter model (Get-OpenApiGenParameterModel)
        and the response type name, and returns the source text of the function: comment-based help,
        [CmdletBinding()] (ShouldProcess for every method except GET/HEAD/OPTIONS unless the verb is Get
        or Test, for every DELETE, and for any verb that changes state), [OutputType()] when the response
        type is known, the param() block and a process block that passes only bound parameters to
        Invoke-OpenApiRequest.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true)]
        [object]$CommandName,

        [Parameter(Mandatory = $true)]
        [object]$Operation,

        [Parameter(Mandatory = $true)]
        [object]$ParameterModel,

        [Parameter()]
        [AllowNull()]
        [AllowEmptyString()]
        [string]$ResponseTypeName,

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

        [Parameter()]
        [AllowNull()]
        [AllowEmptyString()]
        [string]$HelpUri
    )

    $name = $CommandName.Name
    $method = ([string]$Operation.Method).ToUpperInvariant()
    $path = [string]$Operation.Path
    $operationId = [string]$Operation.OperationId
    $parameters = @($ParameterModel.Parameters)
    $stateVerbs = @('New', 'Set', 'Remove', 'Start', 'Stop', 'Restart', 'Reset', 'Update')
    # Get and Test only read, also when the API takes the request as a POST (a query or a validation
    # with a body). A DELETE always asks.
    $readVerbs = @('Get', 'Test')
    $shouldProcess = ($method -eq 'DELETE') -or ($stateVerbs -contains $CommandName.Verb) -or
    ((@('GET', 'HEAD', 'OPTIONS') -notcontains $method) -and ($readVerbs -notcontains $CommandName.Verb))
    $deprecated = ($Operation.Deprecated -eq $true)

    #region help
    # Text from the document goes through ConvertTo-OpenApiGenHelpText, so Get-Help shows plain text and
    # PlatyPS can pass it into MDX; operation ids and paths are code spans.
    $summary = ConvertTo-OpenApiGenHelpText -Text ([string]$Operation.Summary) -SingleLine
    $description = ConvertTo-OpenApiGenHelpText -Text ([string]$Operation.Description)
    $operationCode = '`' + $operationId.Replace('`', '') + '`'
    $requestCode = '`' + "$method $path".Replace('`', '') + '`'
    $synopsis = $summary
    if ($synopsis -eq '' -and $description -ne '') {
        $synopsis = (ConvertTo-OpenApiGenHelpText -Text (($description -split '(?<=\.)\s|\n')[0]) -SingleLine)
    }
    if ($synopsis -eq '') {
        $synopsis = "Calls $requestCode."
    }
    $descriptionText = $description
    if ($descriptionText -eq '') {
        $descriptionText = $summary
    }
    $callText = "Calls the $operationCode operation ($requestCode)."
    if ($descriptionText -eq '') {
        $descriptionText = $callText
    }
    else {
        $descriptionText += "`n`n" + $callText
    }
    if ($deprecated) {
        $descriptionText += "`n`nThis operation is deprecated."
    }
    $sections = New-Object -TypeName System.Collections.ArrayList
    [void]$sections.Add(@{ Keyword = 'SYNOPSIS'; Text = $synopsis })
    [void]$sections.Add(@{ Keyword = 'DESCRIPTION'; Text = $descriptionText })
    foreach ($parameter in $parameters) {
        $text = ConvertTo-OpenApiGenHelpText -Text ([string]$parameter.Description)
        if ($text -eq '') {
            $specCode = '`' + ([string]$parameter.SpecName).Replace('`', '') + '`'
            switch ($parameter.Kind) {
                'Spec' { $text = "The $specCode $($parameter.In) parameter." }
                'BodyProperty' { $text = "The $specCode property of the request body." }
                default { $text = 'See the API documentation.' }
            }
        }
        if ($parameter.Deprecated) {
            $text = 'Deprecated. ' + $text
        }
        [void]$sections.Add(@{ Keyword = 'PARAMETER'; Argument = $parameter.Name; Text = $text })
    }
    # EXAMPLE: the first line is the code, the rest the remarks (Get-Help and PlatyPS read it so)
    $example = $name
    $withRequired = $false
    foreach ($parameter in $parameters) {
        $inDefaultSet = ($null -eq $parameter.ParameterSet) -or ($parameter.ParameterSet -eq 'Parameters')
        if ($parameter.Mandatory -and $inDefaultSet -and $null -ne $parameter.ExampleText) {
            $example += ' -' + $parameter.Name + ' ' + $parameter.ExampleText
            $withRequired = $true
        }
    }
    $remark = "Calls $requestCode"
    if ($withRequired) {
        $remark += ' with example values for the required parameters'
    }
    if ($CommandName.IsList) {
        $remark += ' and returns the items of the first page'
    }
    [void]$sections.Add(@{ Keyword = 'EXAMPLE'; Text = "$example`n`n$remark." })
    if (@($parameters | Where-Object -FilterScript { $_.Name -eq 'All' }).Count -gt 0) {
        [void]$sections.Add(@{ Keyword = 'EXAMPLE'; Text = "$example -All`n`nRequests the following pages as well and returns the items of every page." })
    }
    $outputs = 'System.Management.Automation.PSObject'
    if (-not [string]::IsNullOrEmpty($ResponseTypeName)) {
        $outputs = $ResponseTypeName
    }
    [void]$sections.Add(@{ Keyword = 'OUTPUTS'; Text = $outputs })
    $notes = "Operation: $operationCode ($requestCode)"
    if ($CommandName.IsList) {
        $notes += "`nThis command lists or searches: it can return more than one object."
    }
    if ($deprecated) {
        $notes += "`nThis operation is deprecated and may be removed from the API."
    }
    [void]$sections.Add(@{ Keyword = 'NOTES'; Text = $notes })
    # The first .LINK that is a URI is the online help (Get-Help -Online, PlatyPS 'online version')
    if (-not [string]::IsNullOrWhiteSpace($HelpUri)) {
        [void]$sections.Add(@{ Keyword = 'LINK'; Text = $HelpUri })
    }
    if (-not [string]::IsNullOrWhiteSpace([string]$Operation.ExternalDocsUrl) -and [string]$Operation.ExternalDocsUrl -ne $HelpUri) {
        [void]$sections.Add(@{ Keyword = 'LINK'; Text = [string]$Operation.ExternalDocsUrl })
    }
    $help = ConvertTo-OpenApiGenHelp -Section $sections.ToArray() -Indent 4
    #endregion

    #region attributes
    $attributeLines = New-Object -TypeName System.Collections.ArrayList
    if (@($parameters | Where-Object -FilterScript { $_.Name -match 'password|passphrase' }).Count -gt 0) {
        [void]$attributeLines.Add(" [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '', Justification = 'The API takes this value as plain text.')]")
    }
    $nounWords = @(Split-OpenApiGenWord -Value $CommandName.Noun)
    $uncountable = @('data', 'equipment', 'feedback', 'firmware', 'hardware', 'information', 'media', 'metadata', 'news', 'series', 'software', 'species', 'status')
    if ($nounWords.Count -gt 0 -and $uncountable -contains $nounWords[$nounWords.Count - 1].ToLowerInvariant()) {
        [void]$attributeLines.Add(" [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'The noun comes from the API and has no singular form.')]")
    }
    $bindingArguments = @()
    if (-not [string]::IsNullOrWhiteSpace($HelpUri)) {
        $bindingArguments += 'HelpUri = ' + (ConvertTo-OpenApiGenLiteral -Value $HelpUri)
    }
    if ($ParameterModel.BodyMode -eq 'Flattened') {
        $bindingArguments += "DefaultParameterSetName = 'Parameters'"
    }
    if ($shouldProcess) {
        $impact = 'Medium'
        if ($method -eq 'DELETE') {
            $impact = 'High'
        }
        $bindingArguments += 'SupportsShouldProcess = $true'
        $bindingArguments += "ConfirmImpact = '$impact'"
    }
    [void]$attributeLines.Add(' [CmdletBinding(' + ($bindingArguments -join ', ') + ')]')
    if (-not [string]::IsNullOrEmpty($ResponseTypeName)) {
        [void]$attributeLines.Add(' [OutputType(' + (ConvertTo-OpenApiGenLiteral -Value $ResponseTypeName) + ')]')
    }
    #endregion

    #region param block
    $blocks = @(foreach ($parameter in $parameters) {
            $lines = @()
            $arguments = @()
            if ($parameter.Mandatory) {
                $arguments += 'Mandatory = $true'
            }
            if ($null -ne $parameter.ParameterSet) {
                $arguments += 'ParameterSetName = ' + (ConvertTo-OpenApiGenLiteral -Value $parameter.ParameterSet)
            }
            if ($parameter.Pipeline) {
                $arguments += 'ValueFromPipelineByPropertyName = $true'
            }
            $lines += ' [Parameter(' + ($arguments -join ', ') + ')]'
            if (@($parameter.Aliases).Count -gt 0) {
                $lines += ' [Alias(' + ((@($parameter.Aliases) | ForEach-Object -Process { ConvertTo-OpenApiGenLiteral -Value $_ }) -join ', ') + ')]'
            }
            foreach ($attribute in @($parameter.Attributes)) {
                $lines += ' ' + $attribute
            }
            $lines += ' [' + $parameter.TypeName + ']'
            $lines += ' $' + $parameter.Name
            $lines -join "`n"
        })
    $parameterBlock = $blocks -join ",`n`n"
    #endregion

    #region process block
    $mapped = @($parameters | Where-Object -FilterScript { $_.Kind -eq 'Spec' -or $_.Kind -eq 'BodyProperty' })
    if ($mapped.Count -eq 0) {
        $parameterMap = '@{}'
    }
    else {
        $width = 0
        foreach ($parameter in $mapped) {
            $width = [math]::Max($width, (ConvertTo-OpenApiGenLiteral -Value $parameter.Name).Length)
        }
        $mapLines = @('@{')
        foreach ($parameter in $mapped) {
            $key = (ConvertTo-OpenApiGenLiteral -Value $parameter.Name).PadRight($width)
            $mapLines += ' ' + $key + ' = @(' + (ConvertTo-OpenApiGenLiteral -Value $parameter.In) + ', ' + (ConvertTo-OpenApiGenLiteral -Value $parameter.SpecName) + ')'
        }
        $mapLines += ' }'
        $parameterMap = $mapLines -join "`n"
    }

    $options = New-Object -TypeName System.Collections.ArrayList
    $locations = @(
        @('path', 'PathParameters'),
        @('query', 'QueryParameters'),
        @('header', 'HeaderParameters'),
        @('cookie', 'CookieParameters')
    )
    foreach ($location in $locations) {
        if (@($parameters | Where-Object -FilterScript { $_.Kind -eq 'Spec' -and $_.In -eq $location[0] }).Count -gt 0) {
            [void]$options.Add(" `$tcsRequest['$($location[1])'] = `$tcsValues['$($location[0])']")
        }
    }
    if ($ParameterModel.BodyMode -eq 'Flattened') {
        [void]$options.Add(" if (`$PSCmdlet.ParameterSetName -eq 'Body') {")
        [void]$options.Add(" `$tcsRequest['Body'] = `$Body")
        [void]$options.Add(' }')
        if ($ParameterModel.BodyRequired) {
            [void]$options.Add(' else {')
        }
        else {
            [void]$options.Add(" elseif (`$tcsValues['body'].Count -gt 0) {")
        }
        [void]$options.Add(" `$tcsRequest['Body'] = `$tcsValues['body']")
        [void]$options.Add(' }')
    }
    elseif ($ParameterModel.BodyMode -eq 'Body') {
        [void]$options.Add(" if (`$PSBoundParameters.ContainsKey('Body')) {")
        [void]$options.Add(" `$tcsRequest['Body'] = `$Body")
        [void]$options.Add(' }')
    }
    if (@($ParameterModel.ContentTypes).Count -gt 0) {
        [void]$options.Add(" `$tcsRequest['ContentType'] = " + (ConvertTo-OpenApiGenLiteral -Value @($ParameterModel.ContentTypes)[0]))
        if (@($parameters | Where-Object -FilterScript { $_.Kind -eq 'ContentType' }).Count -gt 0) {
            [void]$options.Add(" if (`$PSBoundParameters.ContainsKey('ContentType')) {")
            [void]$options.Add(" `$tcsRequest['ContentType'] = `$ContentType")
            [void]$options.Add(' }')
        }
    }
    foreach ($switchName in @('All', 'OutFile', 'Raw')) {
        if (@($parameters | Where-Object -FilterScript { $_.Kind -eq $switchName }).Count -gt 0) {
            [void]$options.Add(" if (`$PSBoundParameters.ContainsKey('$switchName')) {")
            [void]$options.Add(" `$tcsRequest['$switchName'] = `$$switchName")
            [void]$options.Add(' }')
        }
    }

    $shouldProcessText = ''
    if ($shouldProcess) {
        $shouldProcessText = @(
            ' $tcsTarget = ' + (ConvertTo-OpenApiGenLiteral -Value $path)
            " foreach (`$tcsKey in @(`$tcsValues['path'].Keys)) {"
            " `$tcsTarget = `$tcsTarget.Replace('{' + `$tcsKey + '}', [string]`$tcsValues['path'][`$tcsKey])"
            ' }'
            ' if (-not $PSCmdlet.ShouldProcess($tcsTarget, ' + (ConvertTo-OpenApiGenLiteral -Value $method) + ')) {'
            ' return'
            ' }'
        ) -join "`n"
    }
    #endregion

    return Expand-OpenApiGenTemplate -Template $Template -Value @{
        FunctionName       = $name
        Help               = $help
        Attributes         = ($attributeLines -join "`n")
        Parameters         = $parameterBlock
        ParameterMap       = $parameterMap
        OperationIdLiteral = (ConvertTo-OpenApiGenLiteral -Value $operationId)
        RequestOptions     = ($options -join "`n")
        ShouldProcess      = $shouldProcessText
    }
}