src/REST/RESTResponse.ps1

# Copyright 2021, Adam Edwards
#
# 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.

ScriptClass RESTResponse {
    $statusCode = strict-val [int]
    $statusDescription = strict-val [string]
    $content = $null
    $headers = strict-val [HashTable]
    $images = $null
    $inputFields = $null
    $links = $null
    $rawContent = $null
    $rawContentLength = strict-val [int]
    $contentTypeData = strict-val [HashTable] $null
    $RequiredContentType = $null

    function __initialize ( $webResponse, [string] $requiredContentType = $null, [bool] $isSyntheticResponse ) {
        if ( $isSyntheticResponse ) {
            $this.statusCode = 0
            $this.content = $webResponse | convertto-json -depth 3
            $this.rawcontent = $this.content
            $this.rawcontentlength = $this.rawcontent.length
        } else {
            $this.statusCode = $webResponse.statusCode
            $this.statusDescription = $webResponse.statusDescription
            $this.rawContent = $webResponse.rawContent
            $this.rawContentLength = $webResponse.rawContentLength
            $this.headers = $webResponse.headers
            $this.content = $webResponse.content
            $this.images = if ( $webResponse | gm images -erroraction ignore ) { $webResponse.images } else { $null }
            $this.inputFields = if ( $webResponse | gm inputfields -erroraction ignore ) { $webResponse.inputfields } else { $null }
            $this.links = if ( $webResponse | gm links -erroraction ignore ) { $webResponse.links } else { $null }
            $this.RequiredContentType = $requiredContentType
        }
        SetContentTypeData $isSyntheticResponse
    }

    static {
        function GetErrorResponseDetails($response) {
            if ( $response -ne $null ) {
                $responseType = $response.GetType()
                $responseStream = switch ( $responseType.FullName ) {
                    'System.Net.WebResponse' {
                        $response.getresponsestream()
                    }
                    'System.Net.HttpWebResponse' {
                        $response.getresponsestream()
                    }
                    'System.Net.Http.HttpResponseMessage' {
                        # No need to read the stream for this type, so
                        # we won't return it
                    }
                    default {
                        # Unknown error type, don't attempt to process it
                        write-verbose "Http request returned as instance of unexpected type '$responseType'"
                    }
                }

                if ( $responseStream -ne $null ) {
                    $reader = New-Object System.IO.StreamReader($responseStream)
                    $errorMessage = $reader.ReadToEnd()
                    $errorMessage
                }
            }
        }
    }

    function SetContentTypeData($isSyntheticResponse) {
        $this.contentTypeData = @{}

        if ( $isSyntheticResponse ) {
            $this.contentTypeData['application/json'] = $true
            return
        }

        if ( $this.headers -ne $null ) {
            $contentType = $this.headers['Content-Type']
            if ( $contentType -ne $null ) {
                $elements = $contentType -split ';'
                $elements | foreach {
                    $elementValue = $_ -split '='
                    if ($elementValue -eq $null) {
                        $elementValue = $_
                    }

                    $this.contentTypeData[$_] = $elementValue
                }
            }
        }
    }

    function HasJsonContent {
        if ( $this.contentTypeData ) {
            ($this.contentTypeData['application/json'] -ne $null)
        } else {
            $false
        }
    }

    function HasXmlContent {
        if ( $this.contentTypeData ) {
            ($this.contentTypeData['application/xml'] -ne $null)
        } else {
            $false
        }
    }

    function GetDeserializedContent([boolean] $includeCorrectedInput = $false) {
        if ( $this.RequiredContentType -ne $null -and $this.RequiredContentType.length -gt 0 ) {
            if ( $this.contentTypeData[$this.RequiredContentType] -eq $null ) {
                $contentTypeHeader = $this.headers['Content-Type']
                throw "Expected content of type '$($this.RequiredContentType)' but content was not actually found in Content-Type Header '$contentTypeHeader'"
            }
        }

        if ( (HasJsonContent) ) {
            $this.content | convertfrom-json
        } elseif ( (HasXmlContent) ) {
            $deserializedContent = DeserializeXml $this.content
            $deserializedContent.deserializedContent
        } else {
            $null
        }
    }

    function DeserializeXml($xmlContent) {
        # Try to deserialize -- this may fail due to
        # the presence of a unicode byte order marker
        $deserializedXml = try {
            [Xml] $xmlContent
        } catch {
            $null
        }

        if ( $deserializedXml -ne $null ) {
            @{
                deserializedContent = $deserializedXml
                correctedXmlContent = $xmlContent
            }
        } else {
            # Remove Byte Order Mark (BOM) that breaks the
            # XML parser during deserialization.
            # We asssume the file is Unicode, i.e UTF16LE with
            # the corresponding BOM.
            $utf8NoBOMEncoding = new-object System.Text.UTF8Encoding $false
            $unicodeBytes = [System.Text.Encoding]::Unicode.GetBytes($response.content)
            $bomOffset = 6
            $utf8NoBOMBytes = [System.Text.Encoding]::Convert(
                [System.Text.Encoding]::Unicode,
                $utf8NoBOMEncoding,
                $unicodeBytes,
                $bomOffset,
                $unicodeBytes.length - $bomOffset
            )
            $utf8NoBOMContent = $utf8NoBOMEncoding.GetString($utf8NoBOMBytes)
            $deserializedCorrectedXml = [Xml] $utf8NoBOMContent
            @{
                deserializedContent = $deserializedCorrectedXml
                correctedXmlContent = $utf8NoBOMContent
            }
        }
    }
}