Functions/Invoke-BBServerRestMethod.ps1

# Copyright 2016 - 2018 WebMD Health Services
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

function Invoke-BBServerRestMethod
{
    <#
    .SYNOPSIS
    Calls a method in the Bitbucket Server REST API.
 
    .DESCRIPTION
    The `Invoke-BBServerRestMethod` function calls a method on the Bitbucket Server REST API. You pass it a Connection object (returned from `New-BBServerConnection`), the HTTP method to use, the name of API (via the `ApiName` parametr), the name/path of the resource via the `ResourcePath` parameter, and a hashtable/object/psobject via the `InputObject` parameter representing the data to send in the body of the request. The data is converted to JSON and sent in the body of the request.
 
    A Bitbucket Server URI has the form `https://example.com/rest/API_NAME/API_VERSION/RESOURCE_PATH`. `API_VERSION` is taken from the connection object passed to the `Connection` parameter. The `API_NAME` path should be passed to the `ApiName` paramter. The `RESOURCE_PATH` path should be passed to the `ResourcePath` parameter. The base URI is taken from the `Uri` property of the connection object passed to the `Connection` parameter.
 
    .EXAMPLE
    $body | Invoke-BBServerRestMethod -Connection $Connection -Method Post -ApiName 'build-status' -ResourcePath ('commits/{0}' -f $commitID)
 
    Demonstrates how to call the /build-status API's `/commits/COMMIT_ID` resource. Body is a hashtable that looks like this:
 
        $body = @{
                    state = 'INPROGRESS';
                    key = 'MY_BUILD_KEY';
                    name = 'MY_BUILD_NAME';
                    url = 'MY_BUILD_URL';
                    description = 'MY_BUILD_DESCRIPTION';
                 }
 
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [object]
        # The connection to use to invoke the REST method.
        $Connection,

        [Parameter(Mandatory=$true)]
        [Microsoft.PowerShell.Commands.WebRequestMethod]
        $Method,

        [Parameter(Mandatory=$true)]
        [string]
        # The name of the API being invoked, e.g. `projects`, `build-status`, etc. If the endpoint URI is `http://example.com/rest/build-status/1.0/commits`, the API name is between the `rest` and API version, which in this example is `build-status`.
        $ApiName,

        [Parameter(Mandatory=$true)]
        [string]
        # The path to the resource to use. If the endpoint URI `http://example.com/rest/build-status/1.0/commits`, the ResourcePath is everything after the API version. In this case, the resource path is `commits`.
        $ResourcePath,

        [Parameter(ValueFromPipeline=$true)]
        [object]
        $InputObject,

        [hashtable]
        # Hashtable representing the request query parameters to include when calling the API resource.
        $Parameter,

        [switch]
        $IsPaged
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $uriPath = 'rest/{0}/{1}/{2}' -f $ApiName.Trim('/'),$Connection.ApiVersion.Trim('/'),$ResourcePath.Trim('/')

    if ($Parameter)
    {
        $requestQueryParameter = ''
        foreach ($key in $Parameter.Keys)
        {
            $requestQueryParameter += '&{0}={1}' -f [uri]::EscapeDataString($key), [uri]::EscapeDataString($Parameter[$key])
        }

        $requestQueryParameter = $requestQueryParameter.TrimStart('&')
        $uriPath = '{0}?{1}' -f $uriPath, $requestQueryParameter
    }

    $uri = New-Object 'Uri' -ArgumentList $Connection.Uri,$uriPath

    $bodyParam = @{ }
    if( $InputObject )
    {
        $bodyParam['Body'] = $InputObject | ConvertTo-Json -Depth 100
    }

    #$DebugPreference = 'Continue'
    Write-Debug -Message ('{0} {1}' -f $Method.ToString().ToUpperInvariant(), $uri)
    if( $bodyParam['Body'] )
    {
        Write-Debug -Message $bodyParam['Body']
    }

    $credential = $Connection.Credential
    $credential = '{0}:{1}' -f $credential.UserName,$credential.GetNetworkCredential().Password

    $authHeaderValue = 'Basic {0}' -f [Convert]::ToBase64String( [Text.Encoding]::UTF8.GetBytes($credential) )
    $headers = @{
        'Authorization' = $authHeaderValue
        'X-Atlassian-Token' = 'no-check'
    }

    try
    {
        if( $IsPaged )
        {
            $nextPageStart = 0
            $isLastPage = $false

            while( $isLastPage -eq $false )
            {
                if ($uriPath -match '\?')
                {
                    $queryStringSeparator = '&'
                }
                else
                {
                    $queryStringSeparator = '?'
                }

                $uriPagedPath = ('{0}{1}limit={2}&start={3}' -f $uriPath, $queryStringSeparator, [int16]::MaxValue, $nextPageStart)
                $uri = New-Object 'Uri' -ArgumentList $Connection.Uri,$uriPagedPath

                $getStream = Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers -ContentType 'application/json' @bodyParam -ErrorVariable 'errors'
                if( $getStream.isLastPage -eq $false )
                {
                    $nextPageStart = $getStream.nextPageStart
                }
                else
                {
                    $isLastPage = $true
                }

                $getStream | Select-Object -ExpandProperty 'values'
            }
        }
        else
        {
            Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers -ContentType 'application/json' @bodyParam -ErrorVariable 'errors'
        }
    }
    catch
    {
        $responseContent = $null
        $exceptionType = $_.Exception.GetType().FullName

        if ($exceptionType -eq 'System.Net.WebException')
        {
            $response = $_.Exception.Response
            if( $response )
            {
                $reader = New-Object 'IO.StreamReader' $response.GetResponseStream()
                $responseContent = $reader.ReadToEnd() | ConvertFrom-Json
                $reader.Dispose()
            }
        }
        elseif (($exceptionType -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -and $_.ErrorDetails)
        {
            try
            {
                $responseContent = $_.ErrorDetails | ConvertFrom-Json
            }
            catch
            {
                $responseContent = $_.ErrorDetails
            }
        }

        for( $idx = 0; $idx -lt $errors.Count; ++$idx )
        {
            if( $Global:Error.Count -gt 0 )
            {
                $Global:Error.RemoveAt(0)
            }
        }

        if( -not $responseContent )
        {
            Write-Error -ErrorRecord $_ -ErrorAction $ErrorActionPreference
            return
        }

        foreach( $item in $responseContent.errors )
        {
            $message = $item.message
            if( $item.context )
            {
                $message ='{0} [context: {1}]' -f $message,$item.context
            }
            if( $item.exceptionName )
            {
                $message = '{0}: {1}' -f $item.exceptionName,$message
            }

            Write-Error -Message $message -ErrorAction $ErrorActionPreference
        }
    }
}