Skip to content

Commit

Permalink
Add response parser
Browse files Browse the repository at this point in the history
  • Loading branch information
evgeniy-scherbina committed Oct 18, 2023
1 parent f5cb0c5 commit b1407ec
Show file tree
Hide file tree
Showing 2 changed files with 208 additions and 0 deletions.
99 changes: 99 additions & 0 deletions service/cachemdw/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package cachemdw

import (
"encoding/json"
"errors"
"fmt"
)

type JsonRpcError struct {
Code int `json:"code"`
Message string `json:"message"`
}

// String returns the string representation of the error
func (e *JsonRpcError) String() string {
return fmt.Sprintf("%s (code: %d)", e.Message, e.Code)
}

// JsonRpcResponse is a EVM JSON-RPC response
type JsonRpcResponse struct {
Version string `json:"jsonrpc,omitempty"`
ID json.RawMessage `json:"id,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
JsonRpcError *JsonRpcError `json:"error,omitempty"`
}

// UnmarshalJsonRpcResponse unmarshals a JSON-RPC response
func UnmarshalJsonRpcResponse(data []byte) (*JsonRpcResponse, error) {
var msg JsonRpcResponse
err := json.Unmarshal(data, &msg)
return &msg, err
}

// Marshal marshals a JSON-RPC response to JSON
func (resp *JsonRpcResponse) Marshal() ([]byte, error) {
return json.Marshal(resp)
}

// Error returns the json-rpc error if any
func (resp *JsonRpcResponse) Error() error {
if resp.JsonRpcError == nil {
return nil
}

return errors.New(resp.JsonRpcError.String())
}

// IsResultEmpty checks if the response's result is empty
func (resp *JsonRpcResponse) IsResultEmpty() bool {
if len(resp.Result) == 0 {
// empty response's result
return true
}

var result interface{}
err := json.Unmarshal(resp.Result, &result)
if err != nil {
// consider result as empty if it's malformed
return true
}

switch r := result.(type) {
case []interface{}:
// consider result as empty if it's empty slice
return len(r) == 0
case string:
// Matches:
// - "" - Empty string
// - "0x0" - Represents zero in official json-rpc conventions. See:
// https://ethereum.org/en/developers/docs/apis/json-rpc/#conventions
//
// - "0x" - Empty response from some endpoints like getCode

return r == "" || r == "0x0" || r == "0x"
case bool:
// consider result as empty if it's false
return !r
case nil:
// consider result as empty if it's null
return true
default:
return false
}
}

// IsCacheable returns true in case of:
// - json-rpc response doesn't contain an error
// - json-rpc response's result isn't empty
func (resp *JsonRpcResponse) IsCacheable() bool {
if err := resp.Error(); err != nil {
return false
}

if resp.IsResultEmpty() {
return false
}

return true
}
109 changes: 109 additions & 0 deletions service/cachemdw/response_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package cachemdw_test

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/require"

"github.com/kava-labs/kava-proxy-service/service/cachemdw"
)

func TestUnitTestJsonRpcResponse_IsEmpty(t *testing.T) {
toJSON := func(t *testing.T, result any) []byte {
resultInJSON, err := json.Marshal(result)
require.NoError(t, err)

return resultInJSON
}

mkResp := func(result []byte) *cachemdw.JsonRpcResponse {
return &cachemdw.JsonRpcResponse{
Version: "2.0",
ID: []byte("1"),
Result: result,
}
}

tests := []struct {
name string
resp *cachemdw.JsonRpcResponse
isEmpty bool
}{
{
name: "empty result",
resp: mkResp([]byte("")),
isEmpty: true,
},
{
name: "invalid json",
resp: mkResp([]byte("invalid json")),
isEmpty: true,
},
{
name: "empty slice",
resp: mkResp(toJSON(t, []interface{}{})),
isEmpty: true,
},
{
name: "empty string",
resp: mkResp(toJSON(t, "")),
isEmpty: true,
},
{
name: "0x0 string",
resp: mkResp(toJSON(t, "0x0")),
isEmpty: true,
},
{
name: "0x string",
resp: mkResp(toJSON(t, "0x")),
isEmpty: true,
},
{
name: "empty bool",
resp: mkResp(toJSON(t, false)),
isEmpty: true,
},
{
name: "nil",
resp: mkResp(nil),
isEmpty: true,
},
{
name: "null",
resp: mkResp(toJSON(t, nil)),
isEmpty: true,
},
{
name: "non-empty slice",
resp: mkResp(toJSON(t, []interface{}{1})),
isEmpty: false,
},
{
name: "non-empty string",
resp: mkResp(toJSON(t, "0x1234")),
isEmpty: false,
},
{
name: "non-empty bool",
resp: mkResp(toJSON(t, true)),
isEmpty: false,
},
{
name: "unsupported empty object",
resp: mkResp(toJSON(t, map[string]interface{}{})),
isEmpty: false,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
require.Equal(
t,
tc.isEmpty,
tc.resp.IsResultEmpty(),
)
})
}
}

0 comments on commit b1407ec

Please sign in to comment.