Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Commit

Permalink
Improve response payload casting operation (#50)
Browse files Browse the repository at this point in the history
`model.Message` struct has a method `CastPayloadToType` used to
conveniently cast `Message.Payload.(Response).Payload` into the type of
the provided argument. The current implementation had a few gaps that
made the method only viaible in situations where `Message.Payload`
were yet to be unmarshalled (e.g. `interface{} | []byte`).

This PR also fixes a bug in rest_service.go where `RestServiceRequest.ResponseType`
was never customizable, leading to the response body to be always
treated like a JSON-decodable structure. This would cause HTTP calls
whose response type is not of JSON to throw errors. By only
deserializing the body for which the request header `Content-Type` is
of JSON type and passing others as raw byte slices, the RestService
callers can handle response payloads of arbitrary MIME types.

Signed-off-by: Josh Kim <[email protected]>
  • Loading branch information
jooskim authored Mar 2, 2022
1 parent 5ca9bcc commit 9ad7c33
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 3 deletions.
21 changes: 18 additions & 3 deletions model/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package model

import (
"encoding/json"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/mitchellh/mapstructure"
Expand Down Expand Up @@ -44,20 +45,34 @@ type MessageHeader struct {
func (m *Message) CastPayloadToType(typ interface{}) error {
var unwrappedResponse Response

// nil-check
// assert pointer type
typVal := reflect.ValueOf(typ)
if typVal.Kind() != reflect.Ptr {
return fmt.Errorf("CastPayloadToType: invalid argument. argument should be the address of an object")
}

// nil-check
if typVal.IsNil() {
return fmt.Errorf("CastPayloadToType: cannot cast to nil")
}

// unwrap payload first
// if message.Payload is already of *Response type, handle it here.
if resp, ok := m.Payload.(*Response); ok {
return decodeResponsePaylod(resp, typ)
}

// otherwise, unmrashal message.Payload into Response, then decode response.Payload
if err := json.Unmarshal(m.Payload.([]byte), &unwrappedResponse); err != nil {
return fmt.Errorf("CastPayloadToType: failed to unmarshal payload %v: %w", m.Payload, err)
}

return mapstructure.Decode(unwrappedResponse.Payload, typ)
return decodeResponsePaylod(&unwrappedResponse, typ)
}

// decodeResponsePaylod tries to unpack Response.Payload into typ.
func decodeResponsePaylod(resp *Response, typ interface{}) error {
if resp.Error {
return errors.New(resp.ErrorMessage)
}
return mapstructure.Decode(resp.Payload, typ)
}
69 changes: 69 additions & 0 deletions model/message_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"reflect"
"testing"
)

Expand Down Expand Up @@ -63,6 +64,47 @@ func TestMessage_CastPayloadToType_NilPointer(t *testing.T) {
assert.Nil(t, dest)
}

func TestMessage_CastPayloadToType_UnmarshalledResponse_ByteSlice(t *testing.T) {
// arrange
msg := getUnmarshalledResponseMessage([]byte("I am a teapot"))
dest := make([]byte, 0)

// act
err := msg.CastPayloadToType(&dest)

// assert
assert.Nil(t, err)
assert.EqualValues(t, []byte("I am a teapot"), dest)
}

func TestMessage_CastPayloadToType_UnmarshalledResponse_Map(t *testing.T) {
// arrange
msg := getUnmarshalledResponseMessage(map[string]interface{}{"418": "I am a teapot"})
dest := make(map[string]string)

// act
err := msg.CastPayloadToType(&dest)
val, keyFound := dest["418"]

// assert
assert.Nil(t, err)
assert.True(t, keyFound)
assert.EqualValues(t, "I am a teapot", val)
}

func TestMessage_CastPayloadToType_ErrorResponse(t *testing.T) {
// arrange
msg := getErrorResponseMessage()
dest := make([]byte, 0)

// act
err := msg.CastPayloadToType(&dest)

// assert
assert.NotNil(t, err)
assert.EqualValues(t, "Bad Request", err.Error())
}

func getNewTestMessage() *Message {
rspPayload := &Response{
Id: &uuid.UUID{},
Expand All @@ -76,3 +118,30 @@ func getNewTestMessage() *Message {
Payload: jsonEncoded,
}
}

func getUnmarshalledResponseMessage(payload interface{}) *Message {
rspPayload := &Response{
Id: &uuid.UUID{},
Payload: payload,
}

return &Message{
Id: &uuid.UUID{},
Channel: "test",
Payload: reflect.ValueOf(rspPayload).Interface(),
}
}

func getErrorResponseMessage() *Message {
rspPayload := &Response{
Id: &uuid.UUID{},
Error: true,
ErrorCode: 400,
ErrorMessage: "Bad Request",
}
return &Message{
Id: &uuid.UUID{},
Channel: "test",
Payload: reflect.ValueOf(rspPayload).Interface(),
}
}
10 changes: 10 additions & 0 deletions service/rest_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net/http"
"net/url"
"reflect"
"strings"
)

const (
Expand Down Expand Up @@ -94,6 +95,15 @@ func (rs *restService) HandleServiceRequest(request *model.Request, core FabricS
httpReq.Header.Add("Content-Type", "application/merge-patch+json")
}

contentType := httpReq.Header.Get("Content-Type")
if strings.Contains(contentType, "json") {
// leaving restReq.ResponseType empty is equivalent to treating the response as JSON. see deserializeResponse().
} else {
// otherwise default to byte slice. note that we have an arm for the string type, but defaulting to the byte
// slice makes the payload more flexible to handle in downstream consumers
restReq.ResponseType = reflect.TypeOf([]byte{})
}

httpResp, err := rs.httpClient.Do(httpReq)
if err != nil {
core.SendErrorResponse(request, 500, err.Error())
Expand Down

0 comments on commit 9ad7c33

Please sign in to comment.