Skip to content

Commit

Permalink
Api funcs (#63)
Browse files Browse the repository at this point in the history
Add API utility functions.
  • Loading branch information
john-phillips-arista authored Sep 21, 2022
1 parent e7cebd8 commit b877c85
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 4 deletions.
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ module github.com/untangle/golang-shared
go 1.12

require (
github.com/golang/protobuf v1.5.2 // indirect

github.com/golang/protobuf v1.5.2
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oschwald/geoip2-golang v1.8.0
github.com/pebbe/zmq4 v1.2.9
github.com/r3labs/diff/v2 v2.15.1
Expand Down
5 changes: 2 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand All @@ -8,6 +7,8 @@ github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs=
github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw=
github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg=
Expand All @@ -20,7 +21,6 @@ github.com/r3labs/diff/v2 v2.15.1 h1:EOrVqPUzi+njlumoqJwiS/TgGgmZo83619FNDB9xQUg
github.com/r3labs/diff/v2 v2.15.1/go.mod h1:I8noH9Fc2fjSaMxqF3G2lhDdC0b+JXCfyx85tWFM9kc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.3/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
Expand Down Expand Up @@ -53,7 +53,6 @@ google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
92 changes: 92 additions & 0 deletions util/http/responses.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
package http

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"

"github.com/mitchellh/mapstructure"
)

// Message is a message from the API.
type Message struct {
Message string `json:"message"`
Expand All @@ -17,3 +26,86 @@ type ErrorResponse struct {
Type string `json:"type"`
Messages []*Message `json:"messages"`
}

func (e *ErrorResponse) Error() string {
msgString := ""
for _, msg := range e.Messages {
msgString += fmt.Sprintf("%+v", msg)
}
return fmt.Sprintf("http response error, type: %s, messages: %+v", e.Type, msgString)
}

// ToApiError given error will return the ErrorResponse that
// implements err if it is an ErrorResponse, nil otherwise.
func ToApiError(err error) *ErrorResponse {
switch e := err.(type) {
case *ErrorResponse:
return e
default:
return nil
}
}

// IsApiError -- return true if err is/has an *ErrorResponse.
func IsApiError(err error) bool {
return ToApiError(err) != nil
}

func unmarshalResponse(
resp *http.Response,
value interface{}) error {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf(
"unable to read http response: %w",
err)
}
err = json.Unmarshal(body, value)
if err != nil {
return fmt.Errorf(
"unable to unmarshal JSON for http response: %w",
err)
}
return nil
}

// DecodeResponse decodes the HTTP response and returns the response,
// or error. If the returned response is an http error, we return the
// ErrorResponse, which implements the error interface.
func DecodeResponse(resp *http.Response) (*OkayResponse, error) {
if resp.StatusCode < http.StatusMultipleChoices && resp.StatusCode >= http.StatusOK {
ok := &OkayResponse{}
err := unmarshalResponse(resp, ok)
if err != nil {
return nil, fmt.Errorf("decoding successful http response failed: %w", err)
}
return ok, nil
}
errResp := &ErrorResponse{}
err := unmarshalResponse(resp, errResp)
if err != nil {
return nil, fmt.Errorf("decoding error http response failed: %w", err)
}
return nil, errResp

}

// UnmarshalResult will unmarshall the Result member of the
// OkayResponse using mapstructure into the result interface. It will
// use the JSON struct tags to unmarshal.
func (okay *OkayResponse) UnmarshalResult(result interface{}) error {
decoderConfig := mapstructure.DecoderConfig{
TagName: "json",
Result: result,
ErrorUnused: true,
Squash: true,
}
decoder, err := mapstructure.NewDecoder(&decoderConfig)
if err != nil {
return fmt.Errorf("responses: unable to create decoder: %w", err)
}
if err := decoder.Decode(okay.Result); err != nil {
return fmt.Errorf("unable to decode http okay response: %w", err)
}
return nil
}
152 changes: 152 additions & 0 deletions util/http/responses_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package http

import (
"bytes"
"io/ioutil"
"net/http"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestResponseDecode -- test code for decoding and unmarshalling
// responses.
func TestResponseDecode(t *testing.T) {
type resultType struct {
Value1 string `json:"key1"`
Value2 int `json:"key2"`
}
type testResults struct {
// JSON body of response.
json []byte

// http status code of response.
statusCode int

// are we expected to get an error on initial JSON decode?
willErrorOnJSONDecode bool

// are we expected to get an error when unmarshalling?
willErrorOnUnmarshal bool

// the expected value of the unmarshalled result, if
// we don't expect errors.
expected *resultType

// Is the response an api error? If so, we want to
// test the api error methods.
isAPIErrorResponse bool

// messages for either type.
msgs []*Message
}

tests := []testResults{
{
json: []byte(`{
"result": {"key1": "value1", "key2": 2},
"messages": [{"message": "hello, world!", "vars": ["a", "b"]}]
}`),
willErrorOnJSONDecode: false,
expected: &resultType{Value1: "value1", Value2: 2},
statusCode: http.StatusOK,
msgs: []*Message{
{Message: "hello, world!", Variables: []string{"a", "b"}},
},
},
{
json: []byte(
`{"type": "generic error", "messages": [{"message": "x", "vars": ["y"]}]}`),
willErrorOnJSONDecode: true,
statusCode: http.StatusBadRequest,
isAPIErrorResponse: true,
msgs: []*Message{{Message: "x", Variables: []string{"y"}}},
},
{
json: []byte(`{`),
willErrorOnJSONDecode: true,
statusCode: http.StatusOK,
isAPIErrorResponse: false,
},
{
json: []byte(`{
"result": {"key1": "abcdefghijkmlnopqrstuvwxyz abc", "key2": 2000},
"messages": [{"message": "hello, world!", "vars": ["a", "b"]}]
}`),
willErrorOnJSONDecode: false,
expected: &resultType{
Value1: "abcdefghijkmlnopqrstuvwxyz abc",
Value2: 2000},
statusCode: http.StatusOK,
msgs: []*Message{
{Message: "hello, world!", Variables: []string{"a", "b"}},
},
},
{
json: []byte(`{
"result": {"BADKEY": "abcdefghijkmlnopqrstuvwxyz abc", "key2": 2000},
"messages": [{"message": "hello, world!", "vars": ["a", "b"]}]
}`),
willErrorOnJSONDecode: false,
willErrorOnUnmarshal: true,
statusCode: http.StatusOK,
msgs: []*Message{
{Message: "hello, world!", Variables: []string{"a", "b"}},
},
},
{
json: []byte(`{
"result": {"key1": "abcdefghijkmlnopqrstuvwxyz abc", "key2": 2000}
}`),
willErrorOnJSONDecode: false,
expected: &resultType{
Value1: "abcdefghijkmlnopqrstuvwxyz abc",
Value2: 2000},
statusCode: http.StatusOK,
},
}

for _, test := range tests {
jsonValue := test.json
resp := &http.Response{
Status: "OK",
StatusCode: test.statusCode,
Body: ioutil.NopCloser(bytes.NewBuffer(jsonValue)),
}
okay, err := DecodeResponse(resp)

if !test.willErrorOnJSONDecode {
require.Nil(t, err)
assert.Equal(t, test.msgs, okay.Messages)

// test that we can unmarshall the inner
// result, which is of variable type depending
// on the api response.
result := &resultType{}
err = okay.UnmarshalResult(result)

// some of our tests expect this to fail.
if test.willErrorOnUnmarshal {
assert.NotNil(t, err)
} else {
assert.Nil(t, err)
assert.Equal(t, test.expected, result)
}
} else {

assert.NotNil(t, err)
if test.isAPIErrorResponse {

assert.True(t, IsApiError(err))

assert.Regexp(t, `^http response error, type: .*, messages: .*$`,
err.Error())
errorResp := ToApiError(err)
assert.Equal(t, errorResp.Messages, test.msgs)

}
}

}
}

0 comments on commit b877c85

Please sign in to comment.